# 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](https://www.shopdeskbd.com/developers/idempotency.md#idempotency-key).

## The order of things

```text
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

```bash
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

```bash
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](https://www.shopdeskbd.com/developers/guide-photos-by-link.md).

## 4. The product: variants, options and photos in one call

```bash
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[]`:

```json
{
  "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}`:

- **`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](https://www.shopdeskbd.com/developers/reference/inventory.md).

## With the store MCP server

The same build runs from an AI assistant connected to the
[store MCP server](https://www.shopdeskbd.com/developers/ai.md#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_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:

  ```json
  {
    "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](https://www.shopdeskbd.com/developers/guide-photos-by-link.md#from-the-store-mcp-server).
Every tool is on [MCP tools](https://www.shopdeskbd.com/developers/mcp-tools.md).
