Build a catalog through the API
Attributes, a category and a brand, then a product with variants and photos — in four calls.
An importer moving a shop onto Shopdesk, or an assistant setting up a catalogue from a supplier's sheet, needs the whole product — options, a category, a brand, photos — without the dashboard. This guide builds one in four calls: an attribute, a category, a brand, then the product with its variants and photos.
You need a key with products:write (products and their photos) and
catalog:write (attributes, categories and brands). catalog:write is only
granted together with products:read or products:write. Send an
Idempotency-Key on every call below, so a retry never creates a second
anything — see Idempotency.
The order of things
1. POST /attributes Colour: Red, Blue (the options a variant can have)
2. POST /categories Tees, with its image (photos by link)
3. POST /brands Nukta, logo + banner (photos by link)
4. POST /products 2 variants, 5 photos (options and photos by name and position)Attributes come first because a variant's options must already exist: the product write looks them up by name and never creates one. Categories and brands come before the product because it links them by id.
1. An attribute with its values
curl -sS https://api.shopdeskbd.com/ext/v1/attributes \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: 0a4f7c2e-1b3d-4e5f-8a9b-0c1d2e3f4a01" \
-H "Content-Type: application/json" \
-d '{
"name": "Colour",
"type": "variant",
"values": [{ "value": "Red" }, { "value": "Blue" }]
}'type: "variant" makes it an option that defines variants; specification
attributes only describe a product. If the attribute already exists, add the
missing values with POST /attributes/{id}/values instead — a repeated value
is a 409.
2. A category and 3. a brand, with images by link
curl -sS https://api.shopdeskbd.com/ext/v1/categories \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: 0a4f7c2e-1b3d-4e5f-8a9b-0c1d2e3f4a02" \
-H "Content-Type: application/json" \
-d '{ "name": "Tees", "image_url": "https://cdn.example.com/cats/tees.jpg" }'
curl -sS https://api.shopdeskbd.com/ext/v1/brands \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: 0a4f7c2e-1b3d-4e5f-8a9b-0c1d2e3f4a03" \
-H "Content-Type: application/json" \
-d '{
"name": "Nukta",
"logo_url": "https://cdn.example.com/brands/nukta-logo.png",
"banner_url": "https://cdn.example.com/brands/nukta-banner.jpg"
}'Shopdesk downloads each link, checks it is a real image and stores its own
copy; the answer carries the stored URLs, never yours. Keep the id of each.
The rules for links are the same everywhere — see
Photos by link.
4. The product: variants, options and photos in one call
curl -sS https://api.shopdeskbd.com/ext/v1/products \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: 0a4f7c2e-1b3d-4e5f-8a9b-0c1d2e3f4a04" \
-H "Content-Type: application/json" \
-d '{
"name": "Classic Tee",
"status": "active",
"brand_id": "BRAND_ID",
"categories": [{ "id": "CATEGORY_ID", "is_primary": true }],
"images": [
{ "source_url": "https://cdn.example.com/tee/red-front.jpg", "alt_text": "Red tee, front" },
{ "source_url": "https://cdn.example.com/tee/red-back.jpg" },
{ "source_url": "https://cdn.example.com/tee/blue-front.jpg", "alt_text": "Blue tee, front" },
{ "source_url": "https://cdn.example.com/tee/blue-back.jpg" },
{ "source_url": "https://cdn.example.com/tee/detail.jpg" }
],
"variants": [
{
"sku": "TEE-RED",
"name": "Red",
"price": 1250,
"options": [{ "attribute": "Colour", "value": "Red" }],
"image_index": 0
},
{
"sku": "TEE-BLUE",
"name": "Blue",
"price": 1250,
"options": [{ "attribute": "Colour", "value": "Blue" }],
"image_index": 2
}
]
}'What each part does:
images[]is the gallery in display order — up to 10 links per call. The call answers once every photo is stored, so the response already has each photo'sidand storedurl. The first photo is the thumbnail unless one entry says"is_thumbnail": true.options[]names each option by attribute and value — the name (any case), the attribute's slug, or the ids fromGET /attributes. An unknown name is a422on that exact field, such asvariants[1].options[0].value, telling you which call creates it. Two variants of one product cannot have the same options (409).image_indexpoints a variant at a photo by its position in this request'simages[]— the only way to reference a photo that did not exist a moment ago.
One photo that cannot be used fails the whole call with a 422 naming
images[i].source_url, and nothing is written.
The response is the product exactly as GET /products/{id} returns it. Each
variant carries its photo as image and its options as options[]:
{
"sku": "TEE-RED",
"image": { "id": "…", "url": "https://…/gallery/…webp" },
"options": [
{ "attribute_id": "…", "attribute": "Colour", "value_id": "…", "value": "Red" }
]
}Later changes
| To | Call |
|---|---|
| Add photos without touching the rest | POST /products/{id}/images with { "images": [{ "source_url": … }] } (up to 10) |
| Put the photos in a new order | POST /products/{id}/images/reorder with every image id, in order |
| Change one variant's price, sale price, SKU, barcode, weight or photo | PATCH /variants/{identifier} — by id, SKU or barcode |
| Add a variant | PATCH /products/{id} with the full variants list, the new one without an id and with its options |
| Restate the whole gallery | PATCH /products/{id} with images[]: image_id keeps a photo, source_url adds one, and photos left out are deleted |
Two rules to know before a PATCH /products/{id}:
variantsandimagesare full sets. Anything left out is removed — a variant (unless it has stock or order history) or a photo.- Options are set when a variant is created. An existing variant keeps its
options whatever you send;
optionson an entry with anidis refused. To sell a new combination, add a new variant.
Stock is not set on any of these calls: it enters through
POST /inventory/adjustments — see the
Inventory reference.
With the store MCP server
The same build runs from an AI assistant connected to the
store MCP server with a key holding
products:write and catalog:write (which comes with products:read).
Each step above is one tool call:
| Step | Tool | Scopes |
|---|---|---|
| 1. The attribute and its values | create_attribute | catalog:write + products:read |
| 2. The category, with its image | create_category | catalog:write + products:read |
| 3. The brand, with logo and banner | create_brand | catalog:write |
| 4. The product, variants and photos | create_product | products:write |
The tools take the same fields as the REST calls, with three differences made for a conversation:
-
create_attributeis safe to repeat. It looks the attribute up by name (orslug) andtypefirst. If the store already has it, only the values it lacks are added — compared ignoring case, placed after the existing ones — and nothing is renamed or removed, so the second identical call adds nothing.typedefaults tovariant. The answer is{ attribute, created, added_values }. -
A name that matches nothing comes back with candidates. When
create_productnames an option that does not exist, the call fails with nothing created and nothing downloaded, and the error object addscandidatesbesidefields— the store's variant attributes for an unknown attribute, that attribute's values for an unknown value:{ "error": { "code": "VALIDATION_ERROR", "message": "An option name matches nothing in this store, so nothing was created. …", "fields": { "variants[0].options[0].attribute": [ "No attribute \"Color\" in this store. Create it first with `create_attribute` (nothing is created here)." ] }, "candidates": { "variants[0].options[0].attribute": [ { "attribute_id": "…", "attribute": "Colour", "slug": "colour", "type": "variant", "store_id": "…", "values": ["Red", "Blue"] } ] } } }The assistant then uses the right name, or adds what is missing with
create_attribute. Nothing is ever created from a name. -
create_categorytakesparentby name, slug or id instead ofparent_id. A name shared by several categories (two "Shirts" under Men and Women) fails with those categories ascandidates; send the slug or id.
Later changes have their tools too: add_product_images_from_urls adds up to
10 photos from links, and update_variant changes one variant by id, SKU or
barcode. A new variant on update_product takes options like
create_product. Reordering photos, editing or deleting a photo, and
updating categories, brands and attributes stay on REST.
create_product, create_category, create_brand and
add_product_images_from_urls declare openWorldHint: true — they download
the links they are given — so a client that confirms such tools will ask
first. Links follow the same rules as on REST: see
Photos by link.
Every tool is on MCP tools.