> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shoppex.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Product images

> Upload images with the Developer API and set a product's gallery

Two calls put images on a product: upload each image with `POST /dev/v1/media`, then send the returned ids in `image_ids` when you create or update the product.

Both calls need an API key or OAuth token with the `products.write` scope. See [Authentication](/developers/authentication).

> [!TIP]
>
> Using an AI agent? The Shoppex MCP server has the same calls as tools: `media_upload`, then `products_create` or `products_update` with `image_ids`. See [Shoppex in Claude](/agents/claude).

## Upload an image

`POST /dev/v1/media` stores one image and returns its id. Send exactly one of `url` or `data` as JSON:

| Field | Type | Description |
| - | - | - |
| `url` | string | Public `https` URL of the image, up to 10 MB (10,485,760 bytes). Shoppex fetches it once, server side. Private and local addresses are refused, also after a redirect. |
| `data` | string | Base64 image bytes without a `data:` prefix, up to 1 MB (1,048,576 bytes) decoded. Send larger images by `url`. |
| `filename` | string | Optional. Original file name (1–255 characters), kept for the media library. |

Rules:

* Accepted types: PNG, JPEG, GIF and WebP. Shoppex reads the type from the image bytes, not from a header or the file name. SVG and video are refused.
* Limit: 30 uploads per shop in any rolling 60-second window. A request over the limit gets `429`.
* Send an `Idempotency-Key` header (up to 128 characters) so a retry never stores the image twice. A retry with the same key and body returns the first answer.

```bash theme={"system"}
curl -X POST https://api.shoppex.io/dev/v1/media \
  -H "Authorization: Bearer shx_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: media-logo-cover-1" \
  -d '{
    "url": "https://images.example.com/logo-cover.png",
    "filename": "logo-cover.png"
  }'
```

Response (`201 Created`):

```json theme={"system"}
{
  "data": {
    "id": "019c525f-1111-7111-8111-111111111111",
    "url": "https://imagedelivery.net/<hash>/<image>/public",
    "mime_type": "image/png",
    "size": 482113
  }
}
```

| Field | Description |
| - | - |
| `id` | Image id. Pass it in `image_ids` on a product. |
| `url` | Public CDN URL of the image. |
| `mime_type` | `image/png`, `image/jpeg`, `image/gif` or `image/webp`, read from the bytes. |
| `size` | Stored size in bytes. |

### Upload errors

| Status | When |
| - | - |
| `422` | Both or neither of `url` and `data`, a URL that is not public `https` or cannot be fetched, a file over the size cap, or bytes that are not a PNG, JPEG, GIF or WebP image. `error.details[].field` names `url` or `data`. |
| `429` | More than 30 uploads for this shop in the last 60 seconds. |
| `503` | Image storage is unavailable. Read the message: "nothing was stored" means send the same request again with the same key. "did not answer" means send it again with a new `Idempotency-Key`. |

All errors use the standard [error format](/developers/errors).

## Attach images to a product

Send `image_ids` on `POST /dev/v1/products` or `PATCH /dev/v1/products/{id}`:

* At most **10** images per product.
* The order of the array is the gallery order. The first image is the product's main image.
* The array replaces the whole gallery. `[]` removes every image. Leave `image_ids` out to keep the gallery as it is.
* Every id must be an image uploaded to this shop, and each id may appear only once. Otherwise the request fails with `422` and `error.details[].field` is `image_ids`.

The product answer lists the gallery in `images`, in display order:

```json theme={"system"}
{
  "data": {
    "id": "019c525f-2222-7222-8222-222222222222",
    "title": "Logo design",
    "images": [
      { "id": "019c525f-1111-7111-8111-111111111111", "url": "https://imagedelivery.net/<hash>/<image>/public" },
      { "id": "019c525f-3333-7333-8333-333333333333", "url": "https://imagedelivery.net/<hash>/<image>/public" }
    ]
  }
}
```

## Full example

Upload two images, then create a product with both. The cover image becomes the main image.

```bash theme={"system"}
API="https://api.shoppex.io/dev/v1"
AUTH="Authorization: Bearer shx_your_api_key"

COVER=$(curl -s -X POST "$API/media" -H "$AUTH" -H "Content-Type: application/json" \
  -H "Idempotency-Key: media-logo-cover-1" \
  -d '{"url": "https://images.example.com/logo-cover.png"}' | jq -r '.data.id')

PREVIEW=$(curl -s -X POST "$API/media" -H "$AUTH" -H "Content-Type: application/json" \
  -H "Idempotency-Key: media-logo-preview-1" \
  -d '{"url": "https://images.example.com/logo-preview.png"}' | jq -r '.data.id')

curl -X POST "$API/products" -H "$AUTH" -H "Content-Type: application/json" \
  -H "Idempotency-Key: product-logo-design-1" \
  -d "{
    \"title\": \"Logo design\",
    \"price\": 49.99,
    \"type\": \"SERVICE\",
    \"service_text\": \"We deliver the logo files by email within 3 days.\",
    \"image_ids\": [\"$COVER\", \"$PREVIEW\"]
  }"
```

To change the order of an existing product's gallery, send the full list again in the new order:

```bash theme={"system"}
curl -X PATCH "https://api.shoppex.io/dev/v1/products/019c525f-2222-7222-8222-222222222222" \
  -H "Authorization: Bearer shx_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"image_ids": ["019c525f-3333-7333-8333-333333333333", "019c525f-1111-7111-8111-111111111111"]}'
```

## With the SDK

`@shoppexio/sdk` has no dedicated media method yet. Call the endpoint with the client's generic `post`, then use `products.create` or `products.update`:

```typescript theme={"system"}
import { ShoppexClient } from '@shoppexio/sdk';

const client = new ShoppexClient({ apiKey: process.env.SHOPPEX_API_KEY! });

type MediaUpload = { data: { id: string; url: string; mime_type: string; size: number } };

const cover = (await client.post(
  '/dev/v1/media',
  { url: 'https://images.example.com/logo-cover.png', filename: 'logo-cover.png' },
  { idempotencyKey: 'media-logo-cover-1' },
)) as MediaUpload;

const product = await client.products.create(
  {
    title: 'Logo design',
    price: 49.99,
    type: 'SERVICE',
    service_text: 'We deliver the logo files by email within 3 days.',
    image_ids: [cover.data.id],
  },
  { idempotencyKey: 'product-logo-design-1' },
);
```

To read a local file, send it as base64 in `data` (up to 1 MB):

```typescript theme={"system"}
import { readFile } from 'node:fs/promises';

const bytes = await readFile('./logo-cover.png');
const upload = (await client.post('/dev/v1/media', {
  data: bytes.toString('base64'),
  filename: 'logo-cover.png',
})) as MediaUpload;
```

## Reference

The **API reference** tab lists every field of these endpoints under **Products**: `POST /dev/v1/media`, `POST /dev/v1/products` and `PATCH /dev/v1/products/{id}`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.