# Brands

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 brands

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

Every brand of the store; `search` resolves a brand name to its id.

### 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. |
| `status` | `string` | No | Filter to one of the values below. One of: `active`, `inactive`. |
| `search` | `string` | No | Match on name or slug (case-insensitive, partial) — how a name resolves to an id. |

### Request

**cURL**

```bash
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"

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

**JavaScript**

```javascript
const url = new URL("https://api.shopdeskbd.com/ext/v1/brands");
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/brands",
    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/brands?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 brands.
- `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": "Acme",
      "slug": "acme",
      "logo": "https://cdn.example.com/companies/c1/brands/b1/logo.webp",
      "banner_image": "https://cdn.example.com/companies/c1/brands/b1/banner.webp",
      "description": null,
      "seo": {
        "title": "Cotton T-Shirts",
        "description": "Breathable cotton tees in every size."
      },
      "status": "active",
      "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 a brand

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

Creates a brand with an optional logo (`logo_url` or a `logo` part) and banner (`banner_url` or a `banner` part). Send JSON, with images by link, or `multipart/form-data` with the files as parts (at most 10 MB each, 50 MB in all) and the other fields as text (`seo` as a JSON string). An image comes by link or as a part, not both. Images are stored re-encoded as WebP. On a multipart request the `Idempotency-Key` covers the route only — a retry under the same key replays the first answer whatever the form holds — so send a new key for a different request. 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. |
| `slug` | `string` | No | URL slug: lowercase letters, digits and single hyphens (anything else is refused with `422`). Derived from the name when omitted, and suffixed (`-1`, `-2`) when taken. A rename keeps the slug; send one to change it. |
| `status` | `string` (default `active`) | No | — One of: `active`, `inactive`. |
| `seo` | `SeoBodyDto` | No | Search-engine overrides. Send `null` to clear them. In a multipart form, send the object as a JSON string. |
| `banner_url` | `string` | No | Details-page hero banner, by link. An https link the server downloads and stores a copy of (PNG, JPEG, WebP or GIF, at most 10 MB and 8000 px on the longest side; read from the bytes, never the extension). The link itself is not kept. A link that cannot be fetched or is not such an image fails the whole call with `422` naming this field, and nothing is written. Or upload the file in a `banner` part of a multipart request — not both. |
| `logo_url` | `string` | No | Brand logo, by link. An https link the server downloads and stores a copy of (PNG, JPEG, WebP or GIF, at most 10 MB and 8000 px on the longest side; read from the bytes, never the extension). The link itself is not kept. A link that cannot be fetched or is not such an image fails the whole call with `422` naming this field, and nothing is written. Or upload the file in a `logo` part of a multipart request — not both. |
| `name` | `string` | Yes | — |
| `description` | `string` | No | Shown on the brand page. |

### `seo` fields

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | `string` | No | Page title for search engines; stored cut to 200 characters. |
| `description` | `string` | No | Meta description; stored cut to 500 characters. |

### Request

**cURL**

```bash
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"

curl -sS -X POST "https://api.shopdeskbd.com/ext/v1/brands" \
  -H "Authorization: Bearer $SHOPDESK_API_KEY" \
  -H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Nukta",
  "logo_url": "https://cdn.example.com/brands/nukta-logo.png"
}'
```

**JavaScript**

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

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": "Nukta",
    "logo_url": "https://cdn.example.com/brands/nukta-logo.png"
  }),
});

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": "Nukta",
    "logo_url": "https://cdn.example.com/brands/nukta-logo.png",
}

response = requests.post(
    "https://api.shopdeskbd.com/ext/v1/brands",
    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' => 'Nukta',
    'logo_url' => 'https://cdn.example.com/brands/nukta-logo.png',
]);

$ch = curl_init('https://api.shopdeskbd.com/ext/v1/brands');
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 brand.
- `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` — A request with this `Idempotency-Key` is still in flight (`IDEMPOTENCY_IN_FLIGHT`) — retry once it has finished.
- `413` — A file part is larger than 10 MB, or the parts total more than 50 MB.
- `422` — Validation failed. An image link that cannot be downloaded, or a link or file that is not an allowed image, is refused here with `error.fields` naming it.
- `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": "Acme",
  "slug": "acme",
  "logo": "https://cdn.example.com/companies/c1/brands/b1/logo.webp",
  "banner_image": "https://cdn.example.com/companies/c1/brands/b1/banner.webp",
  "description": null,
  "seo": {
    "title": "Cotton T-Shirts",
    "description": "Breathable cotton tees in every size."
  },
  "status": "active",
  "created_at": "2026-08-11T10:00:00.000Z",
  "updated_at": "2026-08-11T10:00:00.000Z"
}
```

## Get a brand by id

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

One brand record.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `uuid` | Yes | The brand 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/brands/REPLACE_WITH_ID" \
  -H "Authorization: Bearer $SHOPDESK_API_KEY"
```

**JavaScript**

```javascript
const url = new URL("https://api.shopdeskbd.com/ext/v1/brands/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/brands/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/brands/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 brand.
- `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` — Brand 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": "Acme",
  "slug": "acme",
  "logo": "https://cdn.example.com/companies/c1/brands/b1/logo.webp",
  "banner_image": "https://cdn.example.com/companies/c1/brands/b1/banner.webp",
  "description": null,
  "seo": {
    "title": "Cotton T-Shirts",
    "description": "Breathable cotton tees in every size."
  },
  "status": "active",
  "created_at": "2026-08-11T10:00:00.000Z",
  "updated_at": "2026-08-11T10:00:00.000Z"
}
```

## Update a brand

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

Changes the fields sent and leaves the rest alone. A new logo or banner replaces the old one, which is deleted only after the new one is saved; `remove_logo` / `remove_banner` delete them. Send JSON, with images by link, or `multipart/form-data` with the files as parts (at most 10 MB each, 50 MB in all) and the other fields as text (`seo` as a JSON string). An image comes by link or as a part, not both. Images are stored re-encoded as WebP. On a multipart request the `Idempotency-Key` covers the route only — a retry under the same key replays the first answer whatever the form holds — so send a new key for a different request. 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 brand 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. |
| `slug` | `string` | No | URL slug: lowercase letters, digits and single hyphens (anything else is refused with `422`). Derived from the name when omitted, and suffixed (`-1`, `-2`) when taken. A rename keeps the slug; send one to change it. |
| `status` | `string` (default `active`) | No | — One of: `active`, `inactive`. |
| `seo` | `SeoBodyDto` | No | Search-engine overrides. Send `null` to clear them. In a multipart form, send the object as a JSON string. |
| `banner_url` | `string` | No | Details-page hero banner, by link. An https link the server downloads and stores a copy of (PNG, JPEG, WebP or GIF, at most 10 MB and 8000 px on the longest side; read from the bytes, never the extension). The link itself is not kept. A link that cannot be fetched or is not such an image fails the whole call with `422` naming this field, and nothing is written. Or upload the file in a `banner` part of a multipart request — not both. |
| `logo_url` | `string` | No | Brand logo, by link. An https link the server downloads and stores a copy of (PNG, JPEG, WebP or GIF, at most 10 MB and 8000 px on the longest side; read from the bytes, never the extension). The link itself is not kept. A link that cannot be fetched or is not such an image fails the whole call with `422` naming this field, and nothing is written. Or upload the file in a `logo` part of a multipart request — not both. |
| `name` | `string` | No | — |
| `description` | `string` | No | Send `null` to clear it. |
| `remove_logo` | `boolean` | No | Remove the current logo. Cannot be combined with a new one. |
| `remove_banner` | `boolean` | No | Remove the current banner. Cannot be combined with a new one. |

### `seo` fields

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | `string` | No | Page title for search engines; stored cut to 200 characters. |
| `description` | `string` | No | Meta description; stored cut to 500 characters. |

### Request

**cURL**

```bash
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"

curl -sS -X PATCH "https://api.shopdeskbd.com/ext/v1/brands/REPLACE_WITH_ID" \
  -H "Authorization: Bearer $SHOPDESK_API_KEY" \
  -H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47" \
  -H "Content-Type: application/json" \
  -d '{
  "description": "Cotton basics since 2019."
}'
```

**JavaScript**

```javascript
const url = new URL("https://api.shopdeskbd.com/ext/v1/brands/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({
    "description": "Cotton basics since 2019."
  }),
});

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 = {
    "description": "Cotton basics since 2019.",
}

response = requests.patch(
    "https://api.shopdeskbd.com/ext/v1/brands/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([
    'description' => 'Cotton basics since 2019.',
]);

$ch = curl_init('https://api.shopdeskbd.com/ext/v1/brands/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 brand.
- `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` — Brand not found.
- `409` — A request with this `Idempotency-Key` is still in flight (`IDEMPOTENCY_IN_FLIGHT`) — retry once it has finished.
- `413` — A file part is larger than 10 MB, or the parts total more than 50 MB.
- `422` — Validation failed, or a remove flag sent with a new image. An image link that cannot be downloaded, or a link or file that is not an allowed image, is refused here with `error.fields` naming it.
- `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": "Acme",
  "slug": "acme",
  "logo": "https://cdn.example.com/companies/c1/brands/b1/logo.webp",
  "banner_image": "https://cdn.example.com/companies/c1/brands/b1/banner.webp",
  "description": null,
  "seo": {
    "title": "Cotton T-Shirts",
    "description": "Breathable cotton tees in every size."
  },
  "status": "active",
  "created_at": "2026-08-11T10:00:00.000Z",
  "updated_at": "2026-08-11T10:00:00.000Z"
}
```
