# Attributes

Part of the [Shopdesk Commerce API reference](https://www.shopdeskbd.com/developers/reference.md). Paths below are shown exactly as the server routes them — prefix them with `https://api.shopdeskbd.com`. Authenticate every request with `Authorization: Bearer shopdesk_sk_YOUR_API_KEY`; each operation names the scope its key must hold. Every failure uses the one error envelope — see [Errors](https://www.shopdeskbd.com/developers/errors.md).

## List attributes

`GET /ext/v1/attributes` · Requires scope `products:read`

Every attribute with its `values[]`. `search` resolves a name such as "Colour" to its id; `type` keeps only variant or specification attributes.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | `number` (default `25`, max 100) | No | Page size. |
| `starting_after` | `string` | No | Return the page AFTER this object id (the previous page's `next_cursor`). |
| `ending_before` | `string` | No | Return the page BEFORE this object id. |
| `sort` | `string` (default `-created_at`) | No | Newest first by default. Only `created_at` is sortable — a cursor over a mutable key (like `updated_at`) cannot page reliably. Use `updated_after` to sync changes. One of: `created_at`, `-created_at`. |
| `store_id` | `string` | No | Restrict to one store. Required breadth control for company-scoped keys; on a store-scoped key it must match the key's own store. |
| `search` | `string` | No | Match on name or slug (case-insensitive, partial) — how a name resolves to an id. |
| `type` | `string` | No | Only `variant` or only `specification` attributes. One of: `variant`, `specification`. |

### Request

**cURL**

```bash
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"

curl -sS "https://api.shopdeskbd.com/ext/v1/attributes?limit=25" \
  -H "Authorization: Bearer $SHOPDESK_API_KEY"
```

**JavaScript**

```javascript
const url = new URL("https://api.shopdeskbd.com/ext/v1/attributes");
Object.entries({
  limit: "25",
}).forEach(([key, value]) => url.searchParams.set(key, value));

const response = await fetch(url, {
  headers: {
    Authorization: `Bearer ${process.env.SHOPDESK_API_KEY}`,
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}

const { data, meta } = await response.json();
```

**Python**

```python
import os

import requests

response = requests.get(
    "https://api.shopdeskbd.com/ext/v1/attributes",
    params={
        "limit": "25",
    },
    headers={
        "Authorization": f"Bearer {os.environ['SHOPDESK_API_KEY']}",
    },
    timeout=30,
)

if not response.ok:
    error = response.json()["error"]
    raise RuntimeError(f"{error['code']}: {error['message']}")

page = response.json()
rows, meta = page["data"], page["meta"]
```

**PHP**

```php
<?php

$key = getenv('SHOPDESK_API_KEY');

$ch = curl_init('https://api.shopdeskbd.com/ext/v1/attributes?limit=25');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key],
    CURLOPT_TIMEOUT => 30,
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$response = json_decode($body, true);

if ($status >= 400) {
    throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}

$rows = $response['data'];
$meta = $response['meta'];
```

### Responses

- `200` — A cursor page of attributes.
- `401` — Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.
- `403` — The key's scopes do not cover this endpoint.
- `422` — Validation failed — `error.fields` maps each offending field to its messages.
- `429` — Rate limit exceeded for this key. Honor `Retry-After` and the `X-RateLimit-*` headers.

**Example 200 response**

```json
{
  "data": [
    {
      "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "name": "Colour",
      "slug": "colour",
      "type": "variant",
      "input_type": "color",
      "is_filterable": true,
      "is_required": false,
      "values": [
        {
          "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
          "attribute_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
          "value": "Red",
          "position": 0,
          "swatch_hex": "#FF0000",
          "swatch_image_url": "https://cdn.example.com/companies/c1/attributes/swatches/swatch.webp",
          "created_at": "2026-08-11T10:00:00.000Z"
        }
      ],
      "created_at": "2026-08-11T10:00:00.000Z",
      "updated_at": "2026-08-11T10:00:00.000Z"
    }
  ],
  "meta": {
    "has_more": true,
    "next_cursor": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001"
  }
}
```

## Create an attribute

`POST /ext/v1/attributes` · Requires scope `catalog:write`

Creates an attribute, optionally with its first `values`. A swatch tile sent as `swatch_image_url` is downloaded and stored by Shopdesk before anything is written. Image links: https only, PNG/JPEG/WebP/GIF read from the bytes (never SVG), at most 10 MB and 8000 px on the longest side (and 40 megapixels in all; 16 for a GIF), fetched within 10 s each and 30 s for the call, at most 50 MB downloaded per call. One failing link fails the whole call and nothing is written. Each image a call carries — link or file — counts as one write against the key's per-minute write limit; calls that download or upload images run at most 2 at a time per key; and a company may have 2,000 links downloaded per UTC day. Past any of these the answer is `429 RATE_LIMITED` with `Retry-After`.

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | `string` | No | Optional, but honored: send it to make retries of this request safe. |

### Body parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `store_id` | `uuid` | No | Target store. REQUIRED with a company-scoped key; with a store-scoped key it may only repeat the key's own store. |
| `name` | `string` | Yes | — |
| `slug` | `string` | No | Lowercase letters, digits and single hyphens (anything else is refused with `422`). Derived from the name when omitted, and suffixed when taken. |
| `type` | `string` | Yes | `variant` attributes make variants (Colour, Size); `specification` attributes only describe a product (Material). It can be changed later only while no product uses the attribute. One of: `variant`, `specification`. |
| `input_type` | `string` (default `text`) | No | How values are shown: `text`, `color` (uses `swatch_hex`) or `image` (uses `swatch_image_url`). Free to change later. One of: `text`, `color`, `image`. |
| `is_filterable` | `boolean` (default `false`) | No | Offer it as a storefront filter. |
| `is_required` | `boolean` (default `false`) | No | — |
| `values` | `AttributeValueInputDto[]` | No | Initial values, in order. A value repeated (ignoring case) is refused with `409`. |

### `values[]` fields

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `value` | `string` | Yes | — |
| `position` | `integer` | No | Display order, lowest first. Inline values default to their place in the list; a value added later defaults to 0. |
| `swatch_hex` | `string` | No | Swatch colour, `#RRGGBB` (stored upper-case). Only on an attribute whose `input_type` is `color` or `image`; sent to a `text` attribute it is refused with `422`, never dropped. |
| `swatch_image_url` | `string` | No | A swatch tile for patterns and fabrics, by https link. The server downloads it, checks it is a PNG, JPEG, WebP or GIF (at most 10 MB and 8000 px on the longest side) and stores a copy; the link itself is never kept. A link that fails fails the whole call with `422` naming this field, and nothing is written. At most 10 links per call. Only on an attribute whose `input_type` is `color` or `image`; sent to a `text` attribute it is refused with `422`, never dropped. |

### Request

**cURL**

```bash
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"

curl -sS -X POST "https://api.shopdeskbd.com/ext/v1/attributes" \
  -H "Authorization: Bearer $SHOPDESK_API_KEY" \
  -H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Colour",
  "type": "variant",
  "values": [
    {
      "value": "Red"
    },
    {
      "value": "Blue"
    }
  ]
}'
```

**JavaScript**

```javascript
const url = new URL("https://api.shopdeskbd.com/ext/v1/attributes");

const response = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SHOPDESK_API_KEY}`,
    "Idempotency-Key": "3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "name": "Colour",
    "type": "variant",
    "values": [
      {
        "value": "Red"
      },
      {
        "value": "Blue"
      }
    ]
  }),
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}

const object = await response.json();
```

**Python**

```python
import os

import requests

payload = {
    "name": "Colour",
    "type": "variant",
    "values": [
        {
            "value": "Red",
        },
        {
            "value": "Blue",
        },
    ],
}

response = requests.post(
    "https://api.shopdeskbd.com/ext/v1/attributes",
    json=payload,
    headers={
        "Authorization": f"Bearer {os.environ['SHOPDESK_API_KEY']}",
        "Idempotency-Key": "3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47",
    },
    timeout=30,
)

if not response.ok:
    error = response.json()["error"]
    raise RuntimeError(f"{error['code']}: {error['message']}")

obj = response.json()
```

**PHP**

```php
<?php

$key = getenv('SHOPDESK_API_KEY');

$payload = json_encode([
    'name' => 'Colour',
    'type' => 'variant',
    'values' => [
        [
            'value' => 'Red',
        ],
        [
            'value' => 'Blue',
        ],
    ],
]);

$ch = curl_init('https://api.shopdeskbd.com/ext/v1/attributes');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47', 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_TIMEOUT => 30,
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$response = json_decode($body, true);

if ($status >= 400) {
    throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}
```

### Responses

- `201` — The new attribute with its values.
- `400` — An `Idempotency-Key` header was sent but is empty or longer than 255 characters (`VALIDATION_ERROR`; `error.fields` names the header).
- `401` — Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.
- `403` — The key's scopes do not cover this endpoint.
- `409` — Two values are the same, ignoring case (`CONFLICT`, `error.fields.values`); or a request with this `Idempotency-Key` is still in flight (`IDEMPOTENCY_IN_FLIGHT`) — retry once it has finished.
- `422` — Validation failed, or a swatch on a `text` attribute. A `swatch_image_url` that cannot be downloaded or is not an allowed image is refused here with `error.fields` naming it (`values[2].swatch_image_url` on create).
- `429` — Rate limit exceeded for this key. Honor `Retry-After` and the `X-RateLimit-*` headers.

**Example 200 response**

```json
{
  "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "name": "Colour",
  "slug": "colour",
  "type": "variant",
  "input_type": "color",
  "is_filterable": true,
  "is_required": false,
  "values": [
    {
      "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "attribute_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "value": "Red",
      "position": 0,
      "swatch_hex": "#FF0000",
      "swatch_image_url": "https://cdn.example.com/companies/c1/attributes/swatches/swatch.webp",
      "created_at": "2026-08-11T10:00:00.000Z"
    }
  ],
  "created_at": "2026-08-11T10:00:00.000Z",
  "updated_at": "2026-08-11T10:00:00.000Z"
}
```

## Get an attribute by id

`GET /ext/v1/attributes/{id}` · Requires scope `products:read`

One attribute with every value, in display order.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `uuid` | Yes | The attribute id. |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `store_id` | `uuid` | No | Restrict the lookup to one store. Breadth control for company-scoped keys; on a store-scoped key it must match the key's own store. |

### Request

**cURL**

```bash
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"

curl -sS "https://api.shopdeskbd.com/ext/v1/attributes/REPLACE_WITH_ID" \
  -H "Authorization: Bearer $SHOPDESK_API_KEY"
```

**JavaScript**

```javascript
const url = new URL("https://api.shopdeskbd.com/ext/v1/attributes/REPLACE_WITH_ID");

const response = await fetch(url, {
  headers: {
    Authorization: `Bearer ${process.env.SHOPDESK_API_KEY}`,
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}

const object = await response.json();
```

**Python**

```python
import os

import requests

response = requests.get(
    "https://api.shopdeskbd.com/ext/v1/attributes/REPLACE_WITH_ID",
    headers={
        "Authorization": f"Bearer {os.environ['SHOPDESK_API_KEY']}",
    },
    timeout=30,
)

if not response.ok:
    error = response.json()["error"]
    raise RuntimeError(f"{error['code']}: {error['message']}")

obj = response.json()
```

**PHP**

```php
<?php

$key = getenv('SHOPDESK_API_KEY');

$ch = curl_init('https://api.shopdeskbd.com/ext/v1/attributes/REPLACE_WITH_ID');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key],
    CURLOPT_TIMEOUT => 30,
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$response = json_decode($body, true);

if ($status >= 400) {
    throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}
```

### Responses

- `200` — The attribute.
- `401` — Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.
- `403` — The key's scopes do not cover this endpoint.
- `404` — Attribute not found.
- `422` — Validation failed — `error.fields` maps each offending field to its messages.
- `429` — Rate limit exceeded for this key. Honor `Retry-After` and the `X-RateLimit-*` headers.

**Example 200 response**

```json
{
  "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "name": "Colour",
  "slug": "colour",
  "type": "variant",
  "input_type": "color",
  "is_filterable": true,
  "is_required": false,
  "values": [
    {
      "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "attribute_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "value": "Red",
      "position": 0,
      "swatch_hex": "#FF0000",
      "swatch_image_url": "https://cdn.example.com/companies/c1/attributes/swatches/swatch.webp",
      "created_at": "2026-08-11T10:00:00.000Z"
    }
  ],
  "created_at": "2026-08-11T10:00:00.000Z",
  "updated_at": "2026-08-11T10:00:00.000Z"
}
```

## Update an attribute

`PATCH /ext/v1/attributes/{id}` · Requires scope `catalog:write`

Renames it or changes its slug, `input_type` or flags. `type` can change only while no product variant or specification uses the attribute. Values are added and edited through `/attributes/{id}/values`. Fields left out are left alone.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `uuid` | Yes | The attribute id. |

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | `string` | No | Optional, but honored: send it to make retries of this request safe. |

### Body parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `store_id` | `uuid` | No | Target store. REQUIRED with a company-scoped key; with a store-scoped key it may only repeat the key's own store. |
| `name` | `string` | No | — |
| `slug` | `string` | No | A rename keeps the slug; send one to change it. |
| `type` | `string` | No | Refused with `409` while any product variant or specification uses the attribute — create a new attribute instead. One of: `variant`, `specification`. |
| `input_type` | `string` | No | Changes only how values are shown. Switching back to `text` keeps the stored swatches, hidden. One of: `text`, `color`, `image`. |
| `is_filterable` | `boolean` | No | — |
| `is_required` | `boolean` | No | — |

### Request

**cURL**

```bash
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"

curl -sS -X PATCH "https://api.shopdeskbd.com/ext/v1/attributes/REPLACE_WITH_ID" \
  -H "Authorization: Bearer $SHOPDESK_API_KEY" \
  -H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Colour",
  "is_filterable": true
}'
```

**JavaScript**

```javascript
const url = new URL("https://api.shopdeskbd.com/ext/v1/attributes/REPLACE_WITH_ID");

const response = await fetch(url, {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.SHOPDESK_API_KEY}`,
    "Idempotency-Key": "3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "name": "Colour",
    "is_filterable": true
  }),
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}

const object = await response.json();
```

**Python**

```python
import os

import requests

payload = {
    "name": "Colour",
    "is_filterable": True,
}

response = requests.patch(
    "https://api.shopdeskbd.com/ext/v1/attributes/REPLACE_WITH_ID",
    json=payload,
    headers={
        "Authorization": f"Bearer {os.environ['SHOPDESK_API_KEY']}",
        "Idempotency-Key": "3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47",
    },
    timeout=30,
)

if not response.ok:
    error = response.json()["error"]
    raise RuntimeError(f"{error['code']}: {error['message']}")

obj = response.json()
```

**PHP**

```php
<?php

$key = getenv('SHOPDESK_API_KEY');

$payload = json_encode([
    'name' => 'Colour',
    'is_filterable' => true,
]);

$ch = curl_init('https://api.shopdeskbd.com/ext/v1/attributes/REPLACE_WITH_ID');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'PATCH',
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47', 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_TIMEOUT => 30,
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$response = json_decode($body, true);

if ($status >= 400) {
    throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}
```

### Responses

- `200` — The updated attribute with its values.
- `400` — An `Idempotency-Key` header was sent but is empty or longer than 255 characters (`VALIDATION_ERROR`; `error.fields` names the header).
- `401` — Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.
- `403` — The key's scopes do not cover this endpoint.
- `404` — Attribute not found.
- `409` — `type` cannot change while products use the attribute (`CONFLICT`, `error.fields.type`); or a request with this `Idempotency-Key` is still in flight (`IDEMPOTENCY_IN_FLIGHT`) — retry once it has finished.
- `422` — Validation failed — `error.fields` maps each offending field to its messages.
- `429` — Rate limit exceeded for this key. Honor `Retry-After` and the `X-RateLimit-*` headers.

**Example 200 response**

```json
{
  "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "name": "Colour",
  "slug": "colour",
  "type": "variant",
  "input_type": "color",
  "is_filterable": true,
  "is_required": false,
  "values": [
    {
      "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "attribute_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "value": "Red",
      "position": 0,
      "swatch_hex": "#FF0000",
      "swatch_image_url": "https://cdn.example.com/companies/c1/attributes/swatches/swatch.webp",
      "created_at": "2026-08-11T10:00:00.000Z"
    }
  ],
  "created_at": "2026-08-11T10:00:00.000Z",
  "updated_at": "2026-08-11T10:00:00.000Z"
}
```

## Add a value to an attribute

`POST /ext/v1/attributes/{id}/values` · Requires scope `catalog:write`

Adds one value, such as "XL" to Size. A `swatch_image_url` is downloaded and stored by Shopdesk before the value is written. Image links: https only, PNG/JPEG/WebP/GIF read from the bytes (never SVG), at most 10 MB and 8000 px on the longest side (and 40 megapixels in all; 16 for a GIF), fetched within 10 s each and 30 s for the call, at most 50 MB downloaded per call. One failing link fails the whole call and nothing is written. Each image a call carries — link or file — counts as one write against the key's per-minute write limit; calls that download or upload images run at most 2 at a time per key; and a company may have 2,000 links downloaded per UTC day. Past any of these the answer is `429 RATE_LIMITED` with `Retry-After`.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `uuid` | Yes | The attribute id. |

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | `string` | No | Optional, but honored: send it to make retries of this request safe. |

### Body parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `store_id` | `uuid` | No | Target store. REQUIRED with a company-scoped key; with a store-scoped key it may only repeat the key's own store. |
| `value` | `string` | Yes | — |
| `position` | `integer` | No | Display order, lowest first. Defaults to 0. |
| `swatch_hex` | `string` | No | Swatch colour, `#RRGGBB` (stored upper-case). Only on an attribute whose `input_type` is `color` or `image`; sent to a `text` attribute it is refused with `422`, never dropped. |
| `swatch_image_url` | `string` | No | A swatch tile for patterns and fabrics, by https link. The server downloads it, checks it is a PNG, JPEG, WebP or GIF (at most 10 MB and 8000 px on the longest side) and stores a copy; the link itself is never kept. A link that fails fails the whole call with `422` naming this field, and nothing is written. At most 10 links per call. Only on an attribute whose `input_type` is `color` or `image`; sent to a `text` attribute it is refused with `422`, never dropped. |

### Request

**cURL**

```bash
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"

curl -sS -X POST "https://api.shopdeskbd.com/ext/v1/attributes/REPLACE_WITH_ID/values" \
  -H "Authorization: Bearer $SHOPDESK_API_KEY" \
  -H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47" \
  -H "Content-Type: application/json" \
  -d '{
  "value": "XL"
}'
```

**JavaScript**

```javascript
const url = new URL("https://api.shopdeskbd.com/ext/v1/attributes/REPLACE_WITH_ID/values");

const response = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SHOPDESK_API_KEY}`,
    "Idempotency-Key": "3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "value": "XL"
  }),
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}

const object = await response.json();
```

**Python**

```python
import os

import requests

payload = {
    "value": "XL",
}

response = requests.post(
    "https://api.shopdeskbd.com/ext/v1/attributes/REPLACE_WITH_ID/values",
    json=payload,
    headers={
        "Authorization": f"Bearer {os.environ['SHOPDESK_API_KEY']}",
        "Idempotency-Key": "3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47",
    },
    timeout=30,
)

if not response.ok:
    error = response.json()["error"]
    raise RuntimeError(f"{error['code']}: {error['message']}")

obj = response.json()
```

**PHP**

```php
<?php

$key = getenv('SHOPDESK_API_KEY');

$payload = json_encode([
    'value' => 'XL',
]);

$ch = curl_init('https://api.shopdeskbd.com/ext/v1/attributes/REPLACE_WITH_ID/values');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47', 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_TIMEOUT => 30,
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$response = json_decode($body, true);

if ($status >= 400) {
    throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}
```

### Responses

- `201` — The new value.
- `400` — An `Idempotency-Key` header was sent but is empty or longer than 255 characters (`VALIDATION_ERROR`; `error.fields` names the header).
- `401` — Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.
- `403` — The key's scopes do not cover this endpoint.
- `404` — Attribute not found.
- `409` — The attribute already has this value (`CONFLICT`, `error.fields.value`); or a request with this `Idempotency-Key` is still in flight (`IDEMPOTENCY_IN_FLIGHT`) — retry once it has finished.
- `422` — Validation failed, or a swatch on a `text` attribute. A `swatch_image_url` that cannot be downloaded or is not an allowed image is refused here with `error.fields` naming it (`values[2].swatch_image_url` on create).
- `429` — Rate limit exceeded for this key. Honor `Retry-After` and the `X-RateLimit-*` headers.

**Example 200 response**

```json
{
  "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "attribute_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "value": "Red",
  "position": 0,
  "swatch_hex": "#FF0000",
  "swatch_image_url": "https://cdn.example.com/companies/c1/attributes/swatches/swatch.webp",
  "created_at": "2026-08-11T10:00:00.000Z"
}
```

## Update an attribute value

`PATCH /ext/v1/attributes/{id}/values/{valueId}` · Requires scope `catalog:write`

Renames a value, moves it, or sets or clears its swatch. Renaming relabels every variant and product that shows it. A new `swatch_image_url` is downloaded and stored by Shopdesk. Image links: https only, PNG/JPEG/WebP/GIF read from the bytes (never SVG), at most 10 MB and 8000 px on the longest side (and 40 megapixels in all; 16 for a GIF), fetched within 10 s each and 30 s for the call, at most 50 MB downloaded per call. One failing link fails the whole call and nothing is written. Each image a call carries — link or file — counts as one write against the key's per-minute write limit; calls that download or upload images run at most 2 at a time per key; and a company may have 2,000 links downloaded per UTC day. Past any of these the answer is `429 RATE_LIMITED` with `Retry-After`.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `uuid` | Yes | The attribute id. |
| `valueId` | `uuid` | Yes | The value id, as listed in the attribute's `values[]`. |

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | `string` | No | Optional, but honored: send it to make retries of this request safe. |

### Body parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `store_id` | `uuid` | No | Target store. REQUIRED with a company-scoped key; with a store-scoped key it may only repeat the key's own store. |
| `value` | `string` | No | Renaming relabels every variant and product that shows this value. |
| `position` | `integer` | No | — |
| `swatch_hex` | `string` | No | Swatch colour, `#RRGGBB`; `null` clears it. Only on an attribute whose `input_type` is `color` or `image`; sent to a `text` attribute it is refused with `422`, never dropped. |
| `swatch_image_url` | `string` | No | A swatch tile for patterns and fabrics, by https link. The server downloads it, checks it is a PNG, JPEG, WebP or GIF (at most 10 MB and 8000 px on the longest side) and stores a copy; the link itself is never kept. A link that fails fails the whole call with `422` naming this field, and nothing is written. At most 10 links per call. `null` clears it. Only on an attribute whose `input_type` is `color` or `image`; sent to a `text` attribute it is refused with `422`, never dropped. |

### Request

**cURL**

```bash
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"

curl -sS -X PATCH "https://api.shopdeskbd.com/ext/v1/attributes/REPLACE_WITH_ID/values/REPLACE_WITH_ID" \
  -H "Authorization: Bearer $SHOPDESK_API_KEY" \
  -H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47" \
  -H "Content-Type: application/json" \
  -d '{
  "value": "Extra large",
  "position": 4
}'
```

**JavaScript**

```javascript
const url = new URL("https://api.shopdeskbd.com/ext/v1/attributes/REPLACE_WITH_ID/values/REPLACE_WITH_ID");

const response = await fetch(url, {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.SHOPDESK_API_KEY}`,
    "Idempotency-Key": "3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "value": "Extra large",
    "position": 4
  }),
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}

const object = await response.json();
```

**Python**

```python
import os

import requests

payload = {
    "value": "Extra large",
    "position": 4,
}

response = requests.patch(
    "https://api.shopdeskbd.com/ext/v1/attributes/REPLACE_WITH_ID/values/REPLACE_WITH_ID",
    json=payload,
    headers={
        "Authorization": f"Bearer {os.environ['SHOPDESK_API_KEY']}",
        "Idempotency-Key": "3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47",
    },
    timeout=30,
)

if not response.ok:
    error = response.json()["error"]
    raise RuntimeError(f"{error['code']}: {error['message']}")

obj = response.json()
```

**PHP**

```php
<?php

$key = getenv('SHOPDESK_API_KEY');

$payload = json_encode([
    'value' => 'Extra large',
    'position' => 4,
]);

$ch = curl_init('https://api.shopdeskbd.com/ext/v1/attributes/REPLACE_WITH_ID/values/REPLACE_WITH_ID');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'PATCH',
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47', 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_TIMEOUT => 30,
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$response = json_decode($body, true);

if ($status >= 400) {
    throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}
```

### Responses

- `200` — The updated value.
- `400` — An `Idempotency-Key` header was sent but is empty or longer than 255 characters (`VALIDATION_ERROR`; `error.fields` names the header).
- `401` — Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.
- `403` — The key's scopes do not cover this endpoint.
- `404` — Attribute not found, or the value is not on it.
- `409` — Another value of the attribute already has this name (`CONFLICT`, `error.fields.value`); or a request with this `Idempotency-Key` is still in flight (`IDEMPOTENCY_IN_FLIGHT`) — retry once it has finished.
- `422` — Validation failed, or a swatch on a `text` attribute. A `swatch_image_url` that cannot be downloaded or is not an allowed image is refused here with `error.fields` naming it (`values[2].swatch_image_url` on create).
- `429` — Rate limit exceeded for this key. Honor `Retry-After` and the `X-RateLimit-*` headers.

**Example 200 response**

```json
{
  "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "attribute_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "value": "Red",
  "position": 0,
  "swatch_hex": "#FF0000",
  "swatch_image_url": "https://cdn.example.com/companies/c1/attributes/swatches/swatch.webp",
  "created_at": "2026-08-11T10:00:00.000Z"
}
```
