SHOP DESKDevelopers
shopdeskbd.com

Photos by link

Where links are taken, what is checked, all-or-nothing calls, limits and safe retries.

Most catalogues already live somewhere with photo URLs: a supplier feed, an old shop, a spreadsheet. Instead of downloading and re-uploading every file, send the links — Shopdesk downloads them, checks them and stores its own copy. This guide covers where links are taken, the limits, and how to retry safely.

CallFieldScope
POST /productsimages[].source_url (up to 10)products:write
PATCH /products/{id}images[].source_url (up to 10 new; the list is the full gallery)products:write
POST /products/{id}/imagesimages[].source_url in a JSON body (up to 10)products:write
POST / PATCH /categoriesimage_url, banner_urlcatalog:write
POST / PATCH /brandslogo_url, banner_urlcatalog:write
POST /attributes, POST / PATCH /attributes/{id}/valuesswatch_image_urlcatalog:write

If you have the bytes instead of a link, three routes also take the file itself as a multipart/form-data part:

  • POST /products/{id}/images: up to 10 image parts, with each photo's options in an images JSON text field;
  • POST / PATCH /categories: image and banner parts;
  • POST / PATCH /brands: logo and banner parts.

Product create and update, and attribute swatches, take links only — upload product photos as files through POST /products/{id}/images. See the reference for both encodings of each route.

  1. The link is checked before any connection: https only, no user name or password in it, a public host name. It is resolved once, and every address it resolves to must be public — never a private, loopback or link-local address, whether IPv4 or IPv6.
  2. It is downloaded from the address that was checked, following at most 3 redirects, each checked the same way. Each link gets 10 seconds, and all the links of one call get 30 seconds together. Up to 4 download at once.
  3. The bytes are checked: PNG, JPEG, WebP or GIF, read from the bytes — the file name and the server's Content-Type are ignored, and SVG is never accepted. At most 10 MB, 8000 pixels on the longest side and 40 megapixels in all (16 for a GIF), and the whole image must decode.
  4. A copy is stored, re-encoded as WebP with metadata removed. Your link is not kept anywhere; responses carry the stored URL.

All or nothing

A call either stores every photo it was given or none of them.

  • One link fails, the call fails. The answer is 422 VALIDATION_ERROR with fields naming the link — images[3].source_url, logo_url — and a message saying why (not found, not an image, too large, not a public host, too slow). Nothing is written: no product, no variant, no photo.
  • Checked before downloaded. Everything that can be checked without downloading is checked first, so a mistake there costs no download: the shape of images[] (including at most one is_thumbnail), option names, a full gallery, the 30-photo cap, the plan's product limit, the brand and categories, variant SKUs, and a category's parent or a slug.
  • Nothing is left behind. If the save fails after photos were stored, the stored copies are deleted again.

The call answers once every photo is stored, with the photos' final ids — that is what lets a variant point at a photo in the same call (image_index).

Retrying

Send an Idempotency-Key with every call that carries links. On a JSON body the key's fingerprint covers the links, so:

  • A retry with the same key and body answers with the stored response — nothing is downloaded again.
  • The same key with different links is 422 IDEMPOTENCY_CONFLICT: a key names one request.
  • A call that failed releases its key, so after fixing the link you can retry under the same key.

On a multipart upload the key covers the route only, not the files — reuse a key only to retry the same upload. See Idempotency.

Limits

LimitValue
New photos per call10 (send more with further calls)
Photos per product30
Size per image10 MB, 8000 px on the longest side, 40 megapixels (16 for a GIF)
Bytes per call50 MB downloaded, or 50 MB of uploaded files
Time10 s per link, 30 s per call
Writeseach image counts as one write against the per-minute write limit
Calls handling images at once, per key2, downloading or uploading (a third is 429 RATE_LIMITED)
Links per company per UTC day2,000 (then 429 RATE_LIMITED)

A call that would pass a limit is refused before anything is downloaded, and every 429 here carries Retry-After. Ten 9 MB photos pass the per-image limit but not the per-call one: send them over two calls. The daily budget counts links only; uploaded files count against the others. See Rate limits.

Adding photos to a product later

curl -sS https://api.shopdeskbd.com/ext/v1/products/PRODUCT_ID/images \
  -H "Authorization: Bearer $SHOPDESK_API_KEY" \
  -H "Idempotency-Key: 6c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f" \
  -H "Content-Type: application/json" \
  -d '{
    "images": [
      { "source_url": "https://cdn.example.com/tee/side.jpg", "alt_text": "Side view" },
      { "source_url": "https://cdn.example.com/tee/model.jpg", "position": 0 }
    ]
  }'

The batch is one change to the gallery and answers { "data": [...] } — the new photos in the order you sent them. position places a photo (applied in list order); without it a photo is appended.

From the store MCP server

An AI assistant connected to the store MCP server sends links through four tools. Each calls the same service as its REST twin, so everything on this page applies unchanged — the checks, the all-or-nothing rule and the limits, which a key shares between REST and MCP.

ToolTakes links inScope
create_productimages[].source_url (up to 10)products:write
add_product_images_from_urlsimages[].source_url (up to 10)products:write
create_categoryimage_url, banner_urlcatalog:write + products:read
create_brandlogo_url, banner_urlcatalog:write

These four declare openWorldHint: true, because they reach outside the store to download; a client that confirms such tools will ask before running them. Swatch images by link, a full gallery restated on update, and multipart uploads stay on REST.

  • A failed link comes back as a tool result with isError: true, and its text is the REST error object: VALIDATION_ERROR, with fields naming the link (images[1].source_url, logo_url). Nothing was written.
  • Retrying: pass an idempotency_key — the same value on every retry of the same call. A retry with the same key and arguments answers with the first result and downloads nothing; the same key with different links is IDEMPOTENCY_CONFLICT; a call that failed releases its key.
  • Metering: each link counts as one write, as on REST, plus the image limits above (two image calls at once per key, 2,000 links per company per UTC day).