SHOP DESKDevelopers
shopdeskbd.com

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.

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's id and stored url. 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 from GET /attributes. An unknown name is a 422 on that exact field, such as variants[1].options[0].value, telling you which call creates it. Two variants of one product cannot have the same options (409).
  • image_index points a variant at a photo by its position in this request's images[] — 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

ToCall
Add photos without touching the restPOST /products/{id}/images with { "images": [{ "source_url": … }] } (up to 10)
Put the photos in a new orderPOST /products/{id}/images/reorder with every image id, in order
Change one variant's price, sale price, SKU, barcode, weight or photoPATCH /variants/{identifier} — by id, SKU or barcode
Add a variantPATCH /products/{id} with the full variants list, the new one without an id and with its options
Restate the whole galleryPATCH /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}:

  • variants and images are 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; options on an entry with an id is 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:

StepToolScopes
1. The attribute and its valuescreate_attributecatalog:write + products:read
2. The category, with its imagecreate_categorycatalog:write + products:read
3. The brand, with logo and bannercreate_brandcatalog:write
4. The product, variants and photoscreate_productproducts:write

The tools take the same fields as the REST calls, with three differences made for a conversation:

  • create_attribute is safe to repeat. It looks the attribute up by name (or slug) and type first. 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. type defaults to variant. The answer is { attribute, created, added_values }.

  • A name that matches nothing comes back with candidates. When create_product names an option that does not exist, the call fails with nothing created and nothing downloaded, and the error object adds candidates beside fields — 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_category takes parent by name, slug or id instead of parent_id. A name shared by several categories (two "Shirts" under Men and Women) fails with those categories as candidates; 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.