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.
Where a link is taken
| Call | Field | Scope |
|---|---|---|
POST /products | images[].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}/images | images[].source_url in a JSON body (up to 10) | products:write |
POST / PATCH /categories | image_url, banner_url | catalog:write |
POST / PATCH /brands | logo_url, banner_url | catalog:write |
POST /attributes, POST / PATCH /attributes/{id}/values | swatch_image_url | catalog: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 10imageparts, with each photo's options in animagesJSON text field;POST/PATCH /categories:imageandbannerparts;POST/PATCH /brands:logoandbannerparts.
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.
What happens to a link
- The link is checked before any connection:
httpsonly, 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. - 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.
- The bytes are checked: PNG, JPEG, WebP or GIF, read from the bytes —
the file name and the server's
Content-Typeare 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. - 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_ERRORwithfieldsnaming 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 oneis_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
| Limit | Value |
|---|---|
| New photos per call | 10 (send more with further calls) |
| Photos per product | 30 |
| Size per image | 10 MB, 8000 px on the longest side, 40 megapixels (16 for a GIF) |
| Bytes per call | 50 MB downloaded, or 50 MB of uploaded files |
| Time | 10 s per link, 30 s per call |
| Writes | each image counts as one write against the per-minute write limit |
| Calls handling images at once, per key | 2, downloading or uploading (a third is 429 RATE_LIMITED) |
| Links per company per UTC day | 2,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.
| Tool | Takes links in | Scope |
|---|---|---|
create_product | images[].source_url (up to 10) | products:write |
add_product_images_from_urls | images[].source_url (up to 10) | products:write |
create_category | image_url, banner_url | catalog:write + products:read |
create_brand | logo_url, banner_url | catalog: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, withfieldsnaming 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 isIDEMPOTENCY_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).