Categories
The category tree used to organise a store’s catalogue.
Paste a key and every snippet on this page switches from the placeholder to your key — and the Try it panel under each endpoint is ready to send. It is stored in this browser only and goes nowhere except, if you press Send, straight to the API host you pick there.
List categories
/ext/v1/categoriesproducts:readCatalog structure; parent_id builds the tree client-side.
Query parameters
| Name | Type | Description |
|---|---|---|
limit | numberdefault 25max 100 | Page size. |
starting_after | string | Return the page AFTER this object id (the previous page's next_cursor). |
ending_before | string | Return the page BEFORE this object id. |
sort | stringdefault -created_at | 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.created_at-created_at |
store_id | string | 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 | Filter to one of the values below.activeinactive |
search | string | Match on name or slug (case-insensitive, partial) — how a name resolves to an id. |
Request
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS "https://api.shopdeskbd.com/ext/v1/categories?limit=25" \
-H "Authorization: Bearer $SHOPDESK_API_KEY"const url = new URL("https://api.shopdeskbd.com/ext/v1/categories");
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();import os
import requests
response = requests.get(
"https://api.shopdeskbd.com/ext/v1/categories",
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
$key = getenv('SHOPDESK_API_KEY');
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/categories?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'];https://api.shopdeskbd.com/ext/v1/categories?search=shirtsStored in this browser only and never sent anywhere except to the API host you picked above — these pages are static files with no server behind them.
Query parameters
Blank fields are left out of the request.
Paste a key above to enable Send.
Responses
200A cursor page of categories.401Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.403The key's scopes do not cover this endpoint.422Validation failed —error.fieldsmaps each offending field to its messages.429Rate limit exceeded for this key. HonorRetry-Afterand theX-RateLimit-*headers.
Every failure uses the one error envelope — Errors lists each code and what to do with it.
{
"data": [
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"parent_id": null,
"name": "T-Shirts",
"slug": "t-shirts",
"image": "https://cdn.example.com/companies/c1/categories/k1/image.webp",
"banner_image": "https://cdn.example.com/companies/c1/categories/k1/banner.webp",
"description": null,
"seo": {
"title": "Cotton T-Shirts",
"description": "Breathable cotton tees in every size."
},
"status": "active",
"position": 0,
"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 category
/ext/v1/categoriescatalog:writeCreates a category, top-level or under parent_id, with an optional listing image (image_url or an image 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 | Description |
|---|---|---|
Idempotency-Key | string | Optional, but honored: send it to make retries of this request safe. |
Body parameters
| Name | Type | Description |
|---|---|---|
store_id | uuid | Target store. REQUIRED with a company-scoped key; with a store-scoped key it may only repeat the key's own store. |
slug | string | 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 | stringdefault active | —activeinactive |
seo | SeoBodyDto | Search-engine overrides. Send null to clear them. In a multipart form, send the object as a JSON string. |
banner_url | string | 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. |
position | integer | Sort order among siblings, lowest first. |
image_url | string | Listing image, 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 an image part of a multipart request — not both. |
namerequired | string | — |
parent_id | uuid | Parent category, in the same store. Omit for a top-level category. |
description | string | Shown on the category page. |
seo fields
| Name | Type | Description |
|---|---|---|
title | string | Page title for search engines; stored cut to 200 characters. |
description | string | Meta description; stored cut to 500 characters. |
Request
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS -X POST "https://api.shopdeskbd.com/ext/v1/categories" \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47" \
-H "Content-Type: application/json" \
-d '{
"name": "T-Shirts",
"image_url": "https://cdn.example.com/categories/t-shirts.jpg"
}'const url = new URL("https://api.shopdeskbd.com/ext/v1/categories");
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": "T-Shirts",
"image_url": "https://cdn.example.com/categories/t-shirts.jpg"
}),
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}
const object = await response.json();import os
import requests
payload = {
"name": "T-Shirts",
"image_url": "https://cdn.example.com/categories/t-shirts.jpg",
}
response = requests.post(
"https://api.shopdeskbd.com/ext/v1/categories",
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
$key = getenv('SHOPDESK_API_KEY');
$payload = json_encode([
'name' => 'T-Shirts',
'image_url' => 'https://cdn.example.com/categories/t-shirts.jpg',
]);
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/categories');
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']}");
}https://api.shopdeskbd.com/ext/v1/categoriesStored in this browser only and never sent anywhere except to the API host you picked above — these pages are static files with no server behind them.
generating…A new value is generated whenever you edit this request, and kept while you do not — so sending twice without changing anything is a real retry and comes back Idempotent-Replayed: true instead of writing again.
Paste a key above to enable Send.
Responses
201The new category.400AnIdempotency-Keyheader was sent but is empty or longer than 255 characters (VALIDATION_ERROR;error.fieldsnames the header).401Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.403The key's scopes do not cover this endpoint.409A request with thisIdempotency-Keyis still in flight (IDEMPOTENCY_IN_FLIGHT) — retry once it has finished.413A file part is larger than 10 MB, or the parts total more than 50 MB.422Validation failed (an unknownparent_idis named inerror.fields.parent_id). An image link that cannot be downloaded, or a link or file that is not an allowed image, is refused here witherror.fieldsnaming it.429Rate limit exceeded for this key. HonorRetry-Afterand theX-RateLimit-*headers.
Every failure uses the one error envelope — Errors lists each code and what to do with it.
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"parent_id": null,
"name": "T-Shirts",
"slug": "t-shirts",
"image": "https://cdn.example.com/companies/c1/categories/k1/image.webp",
"banner_image": "https://cdn.example.com/companies/c1/categories/k1/banner.webp",
"description": null,
"seo": {
"title": "Cotton T-Shirts",
"description": "Breathable cotton tees in every size."
},
"status": "active",
"position": 0,
"created_at": "2026-08-11T10:00:00.000Z",
"updated_at": "2026-08-11T10:00:00.000Z"
}Get a category by id
/ext/v1/categories/{id}products:readOne category node; follow parent_id for its ancestors.
Path parameters
| Name | Type | Description |
|---|---|---|
idrequired | uuid | The category id. |
Query parameters
| Name | Type | Description |
|---|---|---|
store_id | uuid | 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
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS "https://api.shopdeskbd.com/ext/v1/categories/REPLACE_WITH_ID" \
-H "Authorization: Bearer $SHOPDESK_API_KEY"const url = new URL("https://api.shopdeskbd.com/ext/v1/categories/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();import os
import requests
response = requests.get(
"https://api.shopdeskbd.com/ext/v1/categories/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
$key = getenv('SHOPDESK_API_KEY');
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/categories/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']}");
}https://api.shopdeskbd.com/ext/v1/categories/5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001Stored in this browser only and never sent anywhere except to the API host you picked above — these pages are static files with no server behind them.
Path parameters
Query parameters
Blank fields are left out of the request.
Paste a key above to enable Send.
Responses
200The category.401Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.403The key's scopes do not cover this endpoint.404Category not found.422Validation failed —error.fieldsmaps each offending field to its messages.429Rate limit exceeded for this key. HonorRetry-Afterand theX-RateLimit-*headers.
Every failure uses the one error envelope — Errors lists each code and what to do with it.
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"parent_id": null,
"name": "T-Shirts",
"slug": "t-shirts",
"image": "https://cdn.example.com/companies/c1/categories/k1/image.webp",
"banner_image": "https://cdn.example.com/companies/c1/categories/k1/banner.webp",
"description": null,
"seo": {
"title": "Cotton T-Shirts",
"description": "Breathable cotton tees in every size."
},
"status": "active",
"position": 0,
"created_at": "2026-08-11T10:00:00.000Z",
"updated_at": "2026-08-11T10:00:00.000Z"
}Update a category
/ext/v1/categories/{id}catalog:writeChanges the fields sent and leaves the rest alone. A new image or banner replaces the old one, which is deleted only after the new one is saved; remove_image / remove_banner delete them. parent_id: null makes the category top-level; it cannot move under itself or its own descendants. 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 | Description |
|---|---|---|
idrequired | uuid | The category id. |
Headers
| Name | Type | Description |
|---|---|---|
Idempotency-Key | string | Optional, but honored: send it to make retries of this request safe. |
Body parameters
| Name | Type | Description |
|---|---|---|
store_id | uuid | Target store. REQUIRED with a company-scoped key; with a store-scoped key it may only repeat the key's own store. |
slug | string | 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 | stringdefault active | —activeinactive |
seo | SeoBodyDto | Search-engine overrides. Send null to clear them. In a multipart form, send the object as a JSON string. |
banner_url | string | 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. |
position | integer | Sort order among siblings, lowest first. |
image_url | string | Listing image, 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 an image part of a multipart request — not both. |
name | string | — |
parent_id | uuid | Move under another category of the store, or null to make it top-level. A category cannot move under itself or its own descendants. |
description | string | Send null to clear it. |
remove_image | boolean | Remove the current listing image. Cannot be combined with a new one. |
remove_banner | boolean | Remove the current banner. Cannot be combined with a new one. |
seo fields
| Name | Type | Description |
|---|---|---|
title | string | Page title for search engines; stored cut to 200 characters. |
description | string | Meta description; stored cut to 500 characters. |
Request
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS -X PATCH "https://api.shopdeskbd.com/ext/v1/categories/REPLACE_WITH_ID" \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47" \
-H "Content-Type: application/json" \
-d '{
"name": "Tees",
"position": 2
}'const url = new URL("https://api.shopdeskbd.com/ext/v1/categories/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": "Tees",
"position": 2
}),
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}
const object = await response.json();import os
import requests
payload = {
"name": "Tees",
"position": 2,
}
response = requests.patch(
"https://api.shopdeskbd.com/ext/v1/categories/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
$key = getenv('SHOPDESK_API_KEY');
$payload = json_encode([
'name' => 'Tees',
'position' => 2,
]);
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/categories/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']}");
}https://api.shopdeskbd.com/ext/v1/categories/5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001Stored in this browser only and never sent anywhere except to the API host you picked above — these pages are static files with no server behind them.
Path parameters
generating…A new value is generated whenever you edit this request, and kept while you do not — so sending twice without changing anything is a real retry and comes back Idempotent-Replayed: true instead of writing again.
Paste a key above to enable Send.
Responses
200The updated category.400AnIdempotency-Keyheader was sent but is empty or longer than 255 characters (VALIDATION_ERROR;error.fieldsnames the header).401Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.403The key's scopes do not cover this endpoint.404Category not found.409A request with thisIdempotency-Keyis still in flight (IDEMPOTENCY_IN_FLIGHT) — retry once it has finished.413A file part is larger than 10 MB, or the parts total more than 50 MB.422Validation failed: an unknownparent_idor a move into its own subtree (error.fields.parent_id), 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 witherror.fieldsnaming it.429Rate limit exceeded for this key. HonorRetry-Afterand theX-RateLimit-*headers.
Every failure uses the one error envelope — Errors lists each code and what to do with it.
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"parent_id": null,
"name": "T-Shirts",
"slug": "t-shirts",
"image": "https://cdn.example.com/companies/c1/categories/k1/image.webp",
"banner_image": "https://cdn.example.com/companies/c1/categories/k1/banner.webp",
"description": null,
"seo": {
"title": "Cotton T-Shirts",
"description": "Breathable cotton tees in every size."
},
"status": "active",
"position": 0,
"created_at": "2026-08-11T10:00:00.000Z",
"updated_at": "2026-08-11T10:00:00.000Z"
}