Products
Products with their variants nested — variants are the commercial unit, since price, SKU and stock all hang off them.
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 products
/ext/v1/productsproducts:readVariants are nested in each product — they are the commercial unit. Use updated_after to sync only what changed.
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.draftactivearchived |
category_id | string | Only products in this category. |
updated_after | date-time | Only products changed since this instant — the sync filter. Pair it with the default -created_at order and page with the cursor. |
search | string | Match on product name or variant SKU. |
Request
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS "https://api.shopdeskbd.com/ext/v1/products?limit=25&updated_after=2026-08-01T00%3A00%3A00Z" \
-H "Authorization: Bearer $SHOPDESK_API_KEY"const url = new URL("https://api.shopdeskbd.com/ext/v1/products");
Object.entries({
limit: "25",
updated_after: "2026-08-01T00:00:00Z",
}).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/products",
params={
"limit": "25",
"updated_after": "2026-08-01T00:00:00Z",
},
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/products?limit=25&updated_after=2026-08-01T00%3A00%3A00Z');
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/productsStored 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 products.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",
"name": "Classic Cotton T-Shirt",
"slug": "classic-cotton-t-shirt",
"short_description": null,
"description": null,
"thumbnail": null,
"status": "active",
"delivery_charge": "1250.00",
"rating_average": "4.50",
"rating_count": 12,
"brand": {
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"name": "Acme",
"slug": "acme"
},
"categories": [
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"name": "T-Shirts",
"slug": "t-shirts",
"is_primary": true
}
],
"images": [
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"url": "https://cdn.example.com/p/1.webp",
"position": 0,
"alt_text": "Red cotton tee, front view",
"is_thumbnail": true
}
],
"variants": [
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"product_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"sku": "TSHIRT-RED-M",
"barcode": "0123456789012",
"name": "Red / M",
"price": "1250.00",
"discount_price": "1250.00",
"weight": "0.25",
"dimensions": null,
"low_stock_threshold": 5,
"status": "active",
"image": {
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"url": "https://cdn.example.com/p/1.webp"
},
"options": [
{
"attribute_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"attribute": "Colour",
"value_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"value": "Red"
}
],
"created_at": "2026-08-11T10:00:00.000Z",
"updated_at": "2026-08-11T10:00:00.000Z"
}
],
"videos": [
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"url": "https://cdn.example.com/companies/c/videos/v/video.mp4",
"poster_url": "https://cdn.example.com/companies/c/videos/v/poster-1.webp",
"width": 1080,
"height": 1920,
"duration_ms": 24000,
"has_audio": true,
"title": "Red saree drape",
"gallery_slot": 1,
"sort_order": 0,
"option_value_id": "3c2b1a09-8f7e-4d6c-9b5a-4e3f2d1c0b9a"
}
],
"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 product
/ext/v1/productsproducts:writeAt least one variant (a simple product has exactly one). Slugs auto-generate from the name and auto-suffix when taken; a duplicate SKU is a 409. A whole product can be built in this one call: photos by link in images[] (downloaded and stored before the call answers), each variant’s options[] by attribute and value name (create them first with POST /attributes), and each variant’s photo as image_index into images[]. One photo link that fails fails the whole call and nothing is written. Initial stock is not set here — it enters through POST /v1/inventory/adjustments. 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. |
namerequired | string | — |
slug | string | URL-safe slug. Auto-generated from the name when omitted; auto-suffixed when taken. |
brand_id | uuid | — |
short_description | string | — |
description | string | Rich description, stored as sanitized HTML. ProductsService — the same service the staff endpoints call — strips anything outside the allowlist, so an integration cannot store markup the admin UI could not. Plain text is accepted and kept as-is. An <img> is kept only when its src is on this platform’s own image origin (upload one through the admin POST /products/description-images); an image hosted anywhere else is removed, tag and all. |
status | stringdefault draft | —draftactivearchived |
delivery_charge | number | Per-product delivery-charge override: 0 = free delivery, N = flat charge; omit for the standard shipping rules. |
categories | ProductCategoryLinkDto[] | — |
variantsrequired | ProductVariantWriteDto[] | — |
images | ProductImageWriteDto[] | The gallery, in display order: up to 10 photos by source_url (send more with POST /products/{id}/images). The call returns once every photo is stored; one link that fails fails the whole call and nothing is written. Variants point at a photo with image_index. |
videos | ProductVideoWriteDto[] | E1c: videos to place on the new product — each a video THIS key (or a key it replaced by rotation) started (POST /videos or POST /videos/import), which may still be uploading or verifying; the product shows it once it is ready. Needs the videos:write scope as well as products:write, a plan with product videos, video uploads switched on, and no more than the plan's videos per product. Later changes go through POST /products/{id}/videos. |
categories[] fields
| Name | Type | Description |
|---|---|---|
idrequired | uuid | — |
is_primary | booleandefault false | — |
variants[] fields
| Name | Type | Description |
|---|---|---|
id | uuid | Update only: patches this existing variant. Omit to create a new one. |
skurequired | string | — |
barcode | string | — |
namerequired | string | — |
pricerequired | number | — |
discount_price | number | Sale price — must be strictly below price. |
cost_price | number | What the unit costs the store. Write-only (never serialized, Q6). Omitted on a new variant = 0; omitted on an existing variant (id sent) keeps its current cost. A changed cost on stock in hand is recorded as Stock revaluation on the P&L (only when the old and the new cost are both above 0). |
weight | number | — |
low_stock_threshold | numberdefault 0 | — |
status | stringdefault active | —activeinactive |
options | VariantOptionWriteDto[] | The options that define this variant, e.g. [{ "attribute": "Colour", "value": "Red" }, { "attribute": "Size", "value": "M" }]. Set when the variant is CREATED only (a product create, or an update entry without id); the options of an existing variant cannot be changed here, and an update keeps them. Two variants of a product cannot have the same combination. |
image_index | integer | This variant’s photo, as a zero-based position in THIS request’s images[] — the way to point at a photo sent in the same call. Null clears it. Use image_id instead for a photo the product already has. |
image_id | uuid | Update only: this variant’s photo, as the id of one of the product’s existing photos. Null clears it. Not together with image_index. |
images[] fields
| Name | Type | Description |
|---|---|---|
source_url | string | An https link to a new photo. Shopdesk downloads it, checks the bytes (PNG, JPEG, WebP or GIF; at most 10 MB and 8000 px on the longest side) and stores a re-encoded copy — the link itself is not kept. A link that cannot be used fails the whole call with 422 naming images[i].source_url, and nothing is written. |
image_id | uuid | Update only: an existing photo of this product (an id from images[]), kept in the gallery at this position. |
alt_text | string | Screen-reader alt text. On a kept photo, omit it to leave the stored text alone, or send null to clear it. |
is_thumbnail | booleandefault false | Make this photo the product thumbnail (at most one entry). Without one, a new product takes its first photo, and an update keeps the current thumbnail if it stays in the gallery (else the first photo). |
videos[] fields
| Name | Type | Description |
|---|---|---|
video_idrequired | uuid | A video THIS API key started (POST /videos or POST /videos/import). It may still be uploading or verifying: the product shows it once it is ready. |
gallery_slot | numberdefault 1 | How many of the product's photos come before the video: 0 = before every photo, 1 (default) = right after the main photo. Clamped to the number of photos. |
sort_order | numberdefault 0 | Order among the product's videos; breaks ties within one slot. |
option_value_id | uuid | Show the video for one option value (e.g. Colour: Red — the value id from GET /attributes, or a variant’s options[].value_id). A value one of the product's active variants uses; null or absent = every variant. |
title | string | Its accessible name. Empty or null falls back to the product name. |
options[] fields
| Name | Type | Description |
|---|---|---|
attributerequired | string | A variant attribute of this store: its id, slug or name (any case). |
valuerequired | string | One of that attribute’s values: its id or its text (any case). |
Request
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS -X POST "https://api.shopdeskbd.com/ext/v1/products" \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47" \
-H "Content-Type: application/json" \
-d '{
"name": "Classic Cotton T-Shirt",
"status": "active",
"variants": [
{
"sku": "TSHIRT-RED-M",
"name": "Red / M",
"price": 1250,
"cost_price": 850
}
]
}'const url = new URL("https://api.shopdeskbd.com/ext/v1/products");
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": "Classic Cotton T-Shirt",
"status": "active",
"variants": [
{
"sku": "TSHIRT-RED-M",
"name": "Red / M",
"price": 1250,
"cost_price": 850
}
]
}),
});
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": "Classic Cotton T-Shirt",
"status": "active",
"variants": [
{
"sku": "TSHIRT-RED-M",
"name": "Red / M",
"price": 1250,
"cost_price": 850,
},
],
}
response = requests.post(
"https://api.shopdeskbd.com/ext/v1/products",
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' => 'Classic Cotton T-Shirt',
'status' => 'active',
'variants' => [
[
'sku' => 'TSHIRT-RED-M',
'name' => 'Red / M',
'price' => 1250,
'cost_price' => 850,
],
],
]);
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/products');
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/productsStored 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 created product, variants nested.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 variant SKU is already in use in this store, or two variants have the same options; or a request with thisIdempotency-Keyis still in flight (IDEMPOTENCY_IN_FLIGHT) — retry once it has finished.422Validation failed: an unknown option name (variants[i].options[j].attribute/.value— nothing is created), a photo link that cannot be downloaded or is not an allowed image (images[i].source_url), or a variant photo that is not inimages[].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",
"name": "Classic Cotton T-Shirt",
"slug": "classic-cotton-t-shirt",
"short_description": null,
"description": null,
"thumbnail": null,
"status": "active",
"delivery_charge": "1250.00",
"rating_average": "4.50",
"rating_count": 12,
"brand": {
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"name": "Acme",
"slug": "acme"
},
"categories": [
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"name": "T-Shirts",
"slug": "t-shirts",
"is_primary": true
}
],
"images": [
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"url": "https://cdn.example.com/p/1.webp",
"position": 0,
"alt_text": "Red cotton tee, front view",
"is_thumbnail": true
}
],
"variants": [
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"product_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"sku": "TSHIRT-RED-M",
"barcode": "0123456789012",
"name": "Red / M",
"price": "1250.00",
"discount_price": "1250.00",
"weight": "0.25",
"dimensions": null,
"low_stock_threshold": 5,
"status": "active",
"image": {
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"url": "https://cdn.example.com/p/1.webp"
},
"options": [
{
"attribute_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"attribute": "Colour",
"value_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"value": "Red"
}
],
"created_at": "2026-08-11T10:00:00.000Z",
"updated_at": "2026-08-11T10:00:00.000Z"
}
],
"videos": [
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"url": "https://cdn.example.com/companies/c/videos/v/video.mp4",
"poster_url": "https://cdn.example.com/companies/c/videos/v/poster-1.webp",
"width": 1080,
"height": 1920,
"duration_ms": 24000,
"has_audio": true,
"title": "Red saree drape",
"gallery_slot": 1,
"sort_order": 0,
"option_value_id": "3c2b1a09-8f7e-4d6c-9b5a-4e3f2d1c0b9a"
}
],
"created_at": "2026-08-11T10:00:00.000Z",
"updated_at": "2026-08-11T10:00:00.000Z"
}Get a product by id
/ext/v1/products/{id}products:readThe full product object, variants nested.
Path parameters
| Name | Type | Description |
|---|---|---|
idrequired | uuid | The product 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/products/REPLACE_WITH_ID" \
-H "Authorization: Bearer $SHOPDESK_API_KEY"const url = new URL("https://api.shopdeskbd.com/ext/v1/products/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/products/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/products/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/products/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 product.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.404Product 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",
"name": "Classic Cotton T-Shirt",
"slug": "classic-cotton-t-shirt",
"short_description": null,
"description": null,
"thumbnail": null,
"status": "active",
"delivery_charge": "1250.00",
"rating_average": "4.50",
"rating_count": 12,
"brand": {
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"name": "Acme",
"slug": "acme"
},
"categories": [
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"name": "T-Shirts",
"slug": "t-shirts",
"is_primary": true
}
],
"images": [
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"url": "https://cdn.example.com/p/1.webp",
"position": 0,
"alt_text": "Red cotton tee, front view",
"is_thumbnail": true
}
],
"variants": [
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"product_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"sku": "TSHIRT-RED-M",
"barcode": "0123456789012",
"name": "Red / M",
"price": "1250.00",
"discount_price": "1250.00",
"weight": "0.25",
"dimensions": null,
"low_stock_threshold": 5,
"status": "active",
"image": {
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"url": "https://cdn.example.com/p/1.webp"
},
"options": [
{
"attribute_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"attribute": "Colour",
"value_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"value": "Red"
}
],
"created_at": "2026-08-11T10:00:00.000Z",
"updated_at": "2026-08-11T10:00:00.000Z"
}
],
"videos": [
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"url": "https://cdn.example.com/companies/c/videos/v/video.mp4",
"poster_url": "https://cdn.example.com/companies/c/videos/v/poster-1.webp",
"width": 1080,
"height": 1920,
"duration_ms": 24000,
"has_audio": true,
"title": "Red saree drape",
"gallery_slot": 1,
"sort_order": 0,
"option_value_id": "3c2b1a09-8f7e-4d6c-9b5a-4e3f2d1c0b9a"
}
],
"created_at": "2026-08-11T10:00:00.000Z",
"updated_at": "2026-08-11T10:00:00.000Z"
}Update a product
/ext/v1/products/{id}products:writePartial update of the product fields. categories (when present) replaces the link set; variants (when present) reconciles the FULL variant set — patch by id, create without one, delete the rest unless stock history forbids it. Same semantics as the dashboard. A variant keeps its options; options[] is taken on NEW entries only. images (when present) is the FULL gallery: image_id entries keep those photos, source_url entries add new ones, and photos left out are deleted — to only add photos, use POST /products/{id}/images. 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 product 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. |
name | string | — |
slug | string | — |
brand_id | uuid | Set null to detach the brand. |
short_description | string | — |
description | string | — |
status | string | —draftactivearchived |
delivery_charge | object | Set null to return to the standard shipping rules. |
categories | ProductCategoryLinkDto[] | When present, REPLACES the product's category links. |
variants | ProductVariantWriteDto[] | When present, reconciles the FULL variant set in one transaction — entries with an id are patched, entries without one are created, and existing variants missing from the list are removed (refused when they carry stock history). Same semantics as the dashboard. |
images | ProductImageWriteDto[] | When present, the FULL gallery in its new order, like variants: entries with image_id keep that photo, entries with source_url add a new one (at most 10 per call), and photos missing from the list are DELETED — variants that showed them lose the link. [] empties the gallery. At most 30 photos. To add photos without restating the gallery, use POST /products/{id}/images. |
categories[] fields
| Name | Type | Description |
|---|---|---|
idrequired | uuid | — |
is_primary | booleandefault false | — |
variants[] fields
| Name | Type | Description |
|---|---|---|
id | uuid | Update only: patches this existing variant. Omit to create a new one. |
skurequired | string | — |
barcode | string | — |
namerequired | string | — |
pricerequired | number | — |
discount_price | number | Sale price — must be strictly below price. |
cost_price | number | What the unit costs the store. Write-only (never serialized, Q6). Omitted on a new variant = 0; omitted on an existing variant (id sent) keeps its current cost. A changed cost on stock in hand is recorded as Stock revaluation on the P&L (only when the old and the new cost are both above 0). |
weight | number | — |
low_stock_threshold | numberdefault 0 | — |
status | stringdefault active | —activeinactive |
options | VariantOptionWriteDto[] | The options that define this variant, e.g. [{ "attribute": "Colour", "value": "Red" }, { "attribute": "Size", "value": "M" }]. Set when the variant is CREATED only (a product create, or an update entry without id); the options of an existing variant cannot be changed here, and an update keeps them. Two variants of a product cannot have the same combination. |
image_index | integer | This variant’s photo, as a zero-based position in THIS request’s images[] — the way to point at a photo sent in the same call. Null clears it. Use image_id instead for a photo the product already has. |
image_id | uuid | Update only: this variant’s photo, as the id of one of the product’s existing photos. Null clears it. Not together with image_index. |
images[] fields
| Name | Type | Description |
|---|---|---|
source_url | string | An https link to a new photo. Shopdesk downloads it, checks the bytes (PNG, JPEG, WebP or GIF; at most 10 MB and 8000 px on the longest side) and stores a re-encoded copy — the link itself is not kept. A link that cannot be used fails the whole call with 422 naming images[i].source_url, and nothing is written. |
image_id | uuid | Update only: an existing photo of this product (an id from images[]), kept in the gallery at this position. |
alt_text | string | Screen-reader alt text. On a kept photo, omit it to leave the stored text alone, or send null to clear it. |
is_thumbnail | booleandefault false | Make this photo the product thumbnail (at most one entry). Without one, a new product takes its first photo, and an update keeps the current thumbnail if it stays in the gallery (else the first photo). |
options[] fields
| Name | Type | Description |
|---|---|---|
attributerequired | string | A variant attribute of this store: its id, slug or name (any case). |
valuerequired | string | One of that attribute’s values: its id or its text (any case). |
Request
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS -X PATCH "https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID" \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47" \
-H "Content-Type: application/json" \
-d '{
"status": "active"
}'const url = new URL("https://api.shopdeskbd.com/ext/v1/products/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({
"status": "active"
}),
});
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 = {
"status": "active",
}
response = requests.patch(
"https://api.shopdeskbd.com/ext/v1/products/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([
'status' => 'active',
]);
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/products/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/products/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 product, variants nested.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.404Product not found.409A variant SKU is already in use in this store, or two variants have the same options; or a request with thisIdempotency-Keyis still in flight (IDEMPOTENCY_IN_FLIGHT) — retry once it has finished.422Validation failed:optionssent on an existing variant, an unknown option name, animage_idthat is not a photo of this product, or a photo link that cannot be downloaded or is not an allowed image —error.fieldsnames the field.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",
"name": "Classic Cotton T-Shirt",
"slug": "classic-cotton-t-shirt",
"short_description": null,
"description": null,
"thumbnail": null,
"status": "active",
"delivery_charge": "1250.00",
"rating_average": "4.50",
"rating_count": 12,
"brand": {
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"name": "Acme",
"slug": "acme"
},
"categories": [
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"name": "T-Shirts",
"slug": "t-shirts",
"is_primary": true
}
],
"images": [
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"url": "https://cdn.example.com/p/1.webp",
"position": 0,
"alt_text": "Red cotton tee, front view",
"is_thumbnail": true
}
],
"variants": [
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"product_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"sku": "TSHIRT-RED-M",
"barcode": "0123456789012",
"name": "Red / M",
"price": "1250.00",
"discount_price": "1250.00",
"weight": "0.25",
"dimensions": null,
"low_stock_threshold": 5,
"status": "active",
"image": {
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"url": "https://cdn.example.com/p/1.webp"
},
"options": [
{
"attribute_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"attribute": "Colour",
"value_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"value": "Red"
}
],
"created_at": "2026-08-11T10:00:00.000Z",
"updated_at": "2026-08-11T10:00:00.000Z"
}
],
"videos": [
{
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"url": "https://cdn.example.com/companies/c/videos/v/video.mp4",
"poster_url": "https://cdn.example.com/companies/c/videos/v/poster-1.webp",
"width": 1080,
"height": 1920,
"duration_ms": 24000,
"has_audio": true,
"title": "Red saree drape",
"gallery_slot": 1,
"sort_order": 0,
"option_value_id": "3c2b1a09-8f7e-4d6c-9b5a-4e3f2d1c0b9a"
}
],
"created_at": "2026-08-11T10:00:00.000Z",
"updated_at": "2026-08-11T10:00:00.000Z"
}Archive a product
/ext/v1/products/{id}/archiveproducts:writeNon-destructive: variants, images and inventory survive with their status parked, and the dashboard can restore. Archived products drop out of GET /v1/products.
Path parameters
| Name | Type | Description |
|---|---|---|
idrequired | uuid | The product 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. |
Request
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS -X POST "https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/archive" \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47" \
-H "Content-Type: application/json" \
-d '{}'const url = new URL("https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/archive");
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({}),
});
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 = {}
response = requests.post(
"https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/archive",
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((object) []);
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/archive');
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/products/5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001/archiveStored 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
201Archival confirmed.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.404Product not found.409A request with thisIdempotency-Keyis still in flight (IDEMPOTENCY_IN_FLIGHT) — retry once it has finished.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",
"archived": true
}Add product images
/ext/v1/products/{id}/imagesproducts:writeAdds photos to the product gallery, in one of three ways. **One file:** multipart/form-data with the file in the image part and its options (alt_text, position, is_thumbnail) as text fields; the answer is that image. **Several files:** up to 10 image parts (50 MB in all), with per-file options as a JSON array in one images text field (one entry per part, same order). **Photos by link:** a JSON body { "images": [{ "source_url": "https://…", "alt_text": "…" }] } with up to 10 links, which Shopdesk downloads and stores (the links themselves are not kept). A batch (anything with images, or several parts) answers { "data": [...] }: the new images in the order sent, with their ids. It is all or nothing — one refused file or link fails the call and nothing is added. Images are appended unless position says otherwise; is_thumbnail: true makes one the product thumbnail, and a product with no images yet takes its first image as the thumbnail anyway. A gallery holds at most 30 images. Every file is read from its bytes: PNG, JPEG, WebP or GIF, at most 10 MB and 8000 px on the longest side. 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. Idempotency-Key is honoured here as on every write: on a JSON body the fingerprint covers the links, and a replay answers without downloading again; on a multipart request it covers the route only — reuse a key only when retrying the same files.
Path parameters
| Name | Type | Description |
|---|---|---|
idrequired | uuid | The product 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. |
imagesrequired | ProductImageUploadEntryDto[] | Up to 10 photos, each with the source_url to download, in the order to add them. The call answers { data: [...] }: the new photos in the order sent. |
images[] fields
| Name | Type | Description |
|---|---|---|
source_url | string | JSON body: the https link to download (required). Not allowed in a multipart form, where the entry describes the image part at its index. |
alt_text | string | Screen-reader alt text for this photo. |
position | integer | Zero-based position in the gallery. Existing images shift to make room; a value past the end appends. Applied in list order, as if each photo were uploaded on its own; omit to append. |
is_thumbnail | booleandefault false | Make this photo the product thumbnail (at most one entry). |
Request
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS -X POST "https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/images" \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47" \
-H "Content-Type: application/json" \
-d '{
"images": [
{
"source_url": "https://cdn.example.com/tees/red-front.jpg",
"alt_text": "Red cotton tee, front view",
"is_thumbnail": true
}
]
}'const url = new URL("https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/images");
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({
"images": [
{
"source_url": "https://cdn.example.com/tees/red-front.jpg",
"alt_text": "Red cotton tee, front view",
"is_thumbnail": true
}
]
}),
});
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 = {
"images": [
{
"source_url": "https://cdn.example.com/tees/red-front.jpg",
"alt_text": "Red cotton tee, front view",
"is_thumbnail": True,
},
],
}
response = requests.post(
"https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/images",
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([
'images' => [
[
'source_url' => 'https://cdn.example.com/tees/red-front.jpg',
'alt_text' => 'Red cotton tee, front view',
'is_thumbnail' => true,
],
],
]);
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/images');
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/products/5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001/imagesStored 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
201A single upload: the stored image, as it appears inimages[]. A batch:{ data }, the new images in the order sent.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.404Product not found, or the image is not on this product.409A request with thisIdempotency-Keyis still in flight (IDEMPOTENCY_IN_FLIGHT) — retry once it has finished.413A file is larger than 10 MB, or the files of the request total more than 50 MB.422No image at all, a file or link that is not a readable PNG/JPEG/WebP/GIF (a link that cannot be downloaded is named asimages[i].source_url), an invalid text field, or a gallery that would pass 30 images —error.fieldsnames the field.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",
"url": "https://cdn.example.com/p/1.webp",
"position": 0,
"alt_text": "Red cotton tee, front view",
"is_thumbnail": true
}Update a product image
/ext/v1/products/{id}/images/{imageId}products:writeChanges the alt text, the gallery position and/or which image is the product thumbnail. The bytes cannot be replaced — upload a new image and delete the old one. Fields left out are left alone.
Path parameters
| Name | Type | Description |
|---|---|---|
idrequired | uuid | The product id. |
imageIdrequired | uuid | The image id, as listed in the product’s images[]. |
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. |
alt_text | object | Screen-reader alt text. Set null to clear it. |
position | integer | Zero-based position in the gallery. Existing images shift to make room; a value past the end appends. |
is_thumbnail | boolean | true makes this image the product thumbnail. false on the current thumbnail hands the role to the first image in the gallery. |
Request
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS -X PATCH "https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/images/REPLACE_WITH_ID" \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47" \
-H "Content-Type: application/json" \
-d '{
"alt_text": "Red cotton tee, front view",
"position": 0
}'const url = new URL("https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/images/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({
"alt_text": "Red cotton tee, front view",
"position": 0
}),
});
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 = {
"alt_text": "Red cotton tee, front view",
"position": 0,
}
response = requests.patch(
"https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/images/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([
'alt_text' => 'Red cotton tee, front view',
'position' => 0,
]);
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/images/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/products/5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001/images/9c1e4b2a-7d3f-4e58-b0a1-2f6d8c9e1a45Stored 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 image, as it appears inimages[].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.404Product not found, or the image is not on this product.409A request with thisIdempotency-Keyis still in flight (IDEMPOTENCY_IN_FLIGHT) — retry once it has finished.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",
"url": "https://cdn.example.com/p/1.webp",
"position": 0,
"alt_text": "Red cotton tee, front view",
"is_thumbnail": true
}Delete a product image
/ext/v1/products/{id}/images/{imageId}products:writeRemoves the image from the gallery and from storage — this is permanent, unlike archiving a product. Variants that featured it lose the link; if it was the thumbnail, the first remaining image takes over (or thumbnail becomes null). A company-scoped key names the store with the store_id query parameter, since a DELETE carries no body.
Path parameters
| Name | Type | Description |
|---|---|---|
idrequired | uuid | The product id. |
imageIdrequired | uuid | The image id, as listed in the product’s images[]. |
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. |
Headers
| Name | Type | Description |
|---|---|---|
Idempotency-Key | string | Optional, but honored: send it to make retries of this request safe. |
Request
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS -X DELETE "https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/images/REPLACE_WITH_ID" \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47"const url = new URL("https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/images/REPLACE_WITH_ID");
const response = await fetch(url, {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.SHOPDESK_API_KEY}`,
"Idempotency-Key": "3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47",
},
});
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.delete(
"https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/images/REPLACE_WITH_ID",
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');
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/images/REPLACE_WITH_ID');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47'],
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/products/5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001/images/9c1e4b2a-7d3f-4e58-b0a1-2f6d8c9e1a45Stored 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.
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
200Deletion confirmed.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.404Product not found, or the image is not on this product.409A request with thisIdempotency-Keyis still in flight (IDEMPOTENCY_IN_FLIGHT) — retry once it has finished.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",
"deleted": true
}Reorder product images
/ext/v1/products/{id}/images/reorderproducts:writePuts the whole gallery in a new order: image_ids lists every image of the product exactly once, in the order to show them. A list that leaves an image out or names one twice is refused, so a reorder never removes a photo (delete one with its own DELETE). The thumbnail and alt texts stay as they are. Answers the gallery in its new order.
Path parameters
| Name | Type | Description |
|---|---|---|
idrequired | uuid | The product 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. |
image_idsrequired | string[] | Every image id of the product, each once, in the new display order. A list that leaves one out or repeats one is refused, so a reorder never removes a photo. |
Request
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS -X POST "https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/images/reorder" \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47" \
-H "Content-Type: application/json" \
-d '{
"image_ids": [
"5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"9c1e4b2a-7d3f-4e58-b0a1-2f6d8c9e1a45"
]
}'const url = new URL("https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/images/reorder");
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({
"image_ids": [
"5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"9c1e4b2a-7d3f-4e58-b0a1-2f6d8c9e1a45"
]
}),
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}
const { data } = await response.json();import os
import requests
payload = {
"image_ids": [
"5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"9c1e4b2a-7d3f-4e58-b0a1-2f6d8c9e1a45",
],
}
response = requests.post(
"https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/images/reorder",
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']}")
page = response.json()
rows = page["data"]<?php
$key = getenv('SHOPDESK_API_KEY');
$payload = json_encode([
'image_ids' => [
'5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001',
'9c1e4b2a-7d3f-4e58-b0a1-2f6d8c9e1a45',
],
]);
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/images/reorder');
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']}");
}
$rows = $response['data'];https://api.shopdeskbd.com/ext/v1/products/5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001/images/reorderStored 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 gallery in its new order.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.404Product not found.409A request with thisIdempotency-Keyis still in flight (IDEMPOTENCY_IN_FLIGHT) — retry once it has finished.422image_idsis not exactly the product’s images — one is missing, repeated or not on this product (error.fields.image_ids).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",
"url": "https://cdn.example.com/p/1.webp",
"position": 0,
"alt_text": "Red cotton tee, front view",
"is_thumbnail": true
}
]
}Get a variant by id, SKU or barcode
/ext/v1/variants/{identifier}products:readAnswers "what is this thing in my hand?" for a scanned barcode or a SKU from a spreadsheet.
Path parameters
| Name | Type | Description |
|---|---|---|
identifierrequired | string | A variant id, SKU or barcode. All three resolve here. |
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/variants/SKU-001" \
-H "Authorization: Bearer $SHOPDESK_API_KEY"const url = new URL("https://api.shopdeskbd.com/ext/v1/variants/SKU-001");
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/variants/SKU-001",
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/variants/SKU-001');
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/variants/TSHIRT-RED-MStored 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 variant.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.404Variant 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",
"product_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"sku": "TSHIRT-RED-M",
"barcode": "0123456789012",
"name": "Red / M",
"price": "1250.00",
"discount_price": "1250.00",
"weight": "0.25",
"dimensions": null,
"low_stock_threshold": 5,
"status": "active",
"image": {
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"url": "https://cdn.example.com/p/1.webp"
},
"options": [
{
"attribute_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"attribute": "Colour",
"value_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"value": "Red"
}
],
"created_at": "2026-08-11T10:00:00.000Z",
"updated_at": "2026-08-11T10:00:00.000Z"
}Update a variant
/ext/v1/variants/{identifier}products:writeChanges one variant’s price, discount_price, sku, barcode, weight or photo (image_id) and leaves the rest alone — the narrow edit beside PATCH /products/{id}, which restates the whole variant set. null clears the sale price, barcode, weight or photo. The same rules as the dashboard: a sale price must stay below the price, and the SKU is locked once the variant has stock or order history. Emits product.updated. Answers the variant as GET /variants/{identifier} does.
Path parameters
| Name | Type | Description |
|---|---|---|
identifierrequired | string | The variant id, SKU or barcode — tried in that order, each as an exact match, inside the write store. |
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. |
price | number | — |
discount_price | number | Sale price — must be strictly below the price (the new one, or the stored one when price is not sent). Null removes the sale price. |
sku | string | A new SKU, unique in the store. Refused (409) once the variant has stock or order history. |
barcode | string | Null removes the barcode. |
weight | number | Shipping weight in kg. Null removes it. |
image_id | uuid | This variant’s photo: the id of one of the product’s photos (from images[]). Null clears it, and the storefront falls back to the product thumbnail. |
Request
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS -X PATCH "https://api.shopdeskbd.com/ext/v1/variants/SKU-001" \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47" \
-H "Content-Type: application/json" \
-d '{
"price": 1350,
"discount_price": 1199
}'const url = new URL("https://api.shopdeskbd.com/ext/v1/variants/SKU-001");
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({
"price": 1350,
"discount_price": 1199
}),
});
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 = {
"price": 1350,
"discount_price": 1199,
}
response = requests.patch(
"https://api.shopdeskbd.com/ext/v1/variants/SKU-001",
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([
'price' => 1350,
'discount_price' => 1199,
]);
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/variants/SKU-001');
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/variants/TSHIRT-RED-MStored 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 variant.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.404Variant not found (or its product is archived).409The SKU is taken by another variant, or locked by stock or order history; or a request with thisIdempotency-Keyis still in flight (IDEMPOTENCY_IN_FLIGHT) — retry once it has finished.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",
"product_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"sku": "TSHIRT-RED-M",
"barcode": "0123456789012",
"name": "Red / M",
"price": "1250.00",
"discount_price": "1250.00",
"weight": "0.25",
"dimensions": null,
"low_stock_threshold": 5,
"status": "active",
"image": {
"id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"url": "https://cdn.example.com/p/1.webp"
},
"options": [
{
"attribute_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"attribute": "Colour",
"value_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"value": "Red"
}
],
"created_at": "2026-08-11T10:00:00.000Z",
"updated_at": "2026-08-11T10:00:00.000Z"
}