Videos
Product videos (H.264 MP4, up to 200 MB): upload one straight to storage (start a session, PUT the parts, complete) or import it from a link, set its cover, and place it on products — the server verifies every file.
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.
Place a video on a product
/ext/v1/products/{id}/videosvideos:writePuts one of your videos (from POST /videos or POST /videos/import) on the product: gallery_slot says how many photos come before it (default 1, right after the main photo), sort_order orders it among the product’s videos, option_value_id shows it only for one option value such as Colour: Red (a value one of the product’s active variants uses), and title names it. A video may be placed while it is still uploading or verifying; the product shows it (GET /products/{id} → videos[], the storefront) once it is ready, and a product.updated webhook is sent then. Placing a video the product already shows moves it instead. Nothing else on the product changes. The plan decides how many videos one product may show. A key made by rotating another may place that key’s videos too. Company-scoped keys must send store_id.
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 |
|---|---|---|
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. |
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/videos" \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47" \
-H "Content-Type: application/json" \
-d '{
"video_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"gallery_slot": 1,
"option_value_id": "2c7e9f41-5b3a-4d8e-9f10-6a2b3c4d5e6f",
"title": "Red saree drape"
}'const url = new URL("https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/videos");
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({
"video_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"gallery_slot": 1,
"option_value_id": "2c7e9f41-5b3a-4d8e-9f10-6a2b3c4d5e6f",
"title": "Red saree drape"
}),
});
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 = {
"video_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"gallery_slot": 1,
"option_value_id": "2c7e9f41-5b3a-4d8e-9f10-6a2b3c4d5e6f",
"title": "Red saree drape",
}
response = requests.post(
"https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/videos",
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([
'video_id' => '5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001',
'gallery_slot' => 1,
'option_value_id' => '2c7e9f41-5b3a-4d8e-9f10-6a2b3c4d5e6f',
'title' => 'Red saree drape',
]);
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/products/REPLACE_WITH_ID/videos');
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/videosStored 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 placement, as saved.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.403FORBIDDEN_SCOPE: the key's scopes do not cover this endpoint.PLAN_FEATURE_REQUIRED: the plan does not include product videos.PLAN_LIMIT_REACHED: the product already shows its plan's number of videos (or a released video would not fit the plan’s video storage again).VIDEO_UPLOADS_DISABLED: video uploads are switched off on this platform — no video can be placed (taking one off still works).404Product not found.409A request with thisIdempotency-Keyis still in flight (IDEMPOTENCY_IN_FLIGHT) — retry once it has finished.422Validation failed —error.fieldsnames the field:video_idnames no video this key started in this store, or one that was rejected or whose upload ended;option_value_idis used by none of the product’s active variants.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.
{
"product_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"video_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"status": "verifying",
"gallery_slot": 1,
"sort_order": 0,
"option_value_id": null,
"title": "Red saree drape"
}Take a video off a product
/ext/v1/products/{id}/videos/{videoId}videos:writeThe product stops showing the video; nothing else on it changes. The video itself stays — delete your own upload with DELETE /videos/{id} once no product shows it. Any video the product shows may be taken off, whoever uploaded it — even while video uploads are switched off. 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. |
videoIdrequired | uuid | The video id, as listed in the product’s videos[]. |
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/videos/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/videos/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/videos/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/videos/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/videos/6b1f3c2d-9e8a-4b7c-a6d5-1e2f3a4b5c6dStored 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
200The video is off the product.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.403FORBIDDEN_SCOPE: the key's scopes do not cover this endpoint.PLAN_FEATURE_REQUIRED: the plan does not include product videos.404Product not found, or the product does not show the video.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.
{
"product_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"video_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
"removed": true
}List your video uploads
/ext/v1/videosvideos:writeThe uploads THIS API key started — and, for a key made by rotating another, the uploads of the keys it replaced — newest first, in every status unless status narrows it. Uploads made in the dashboard or by another key are not listed. An unfinished upload past its expiry is left out until it is recorded as aborted. uploaded_parts is empty in a list — read one upload for its parts.
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 | Only uploads in this status. Default: every status.uploadingverifyingreadyrejectedaborted |
Request
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS "https://api.shopdeskbd.com/ext/v1/videos?limit=25" \
-H "Authorization: Bearer $SHOPDESK_API_KEY"const url = new URL("https://api.shopdeskbd.com/ext/v1/videos");
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/videos",
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/videos?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/videosStored 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 page of uploads.401Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.403FORBIDDEN_SCOPE: the key's scopes do not cover this endpoint.PLAN_FEATURE_REQUIRED: the plan does not include product videos.VIDEO_UPLOADS_DISABLED: video uploads are switched off for now.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",
"status": "uploading",
"source": "upload",
"file_name": "red-saree-drape.mp4",
"size_bytes": 48000000,
"part_size": 8388608,
"part_count": 6,
"checksum_algorithm": "SHA256",
"uploaded_parts": [
{
"part_number": 1,
"size": 8388608,
"checksum_sha256": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="
}
],
"uploaded_bytes": 16777216,
"expires_at": "2026-08-11T10:00:00.000Z",
"product_ids": [],
"url": "https://cdn.example.com/companies/c/videos/v/video.mp4",
"poster_url": null,
"facts": {
"duration_ms": 24000,
"width": 1080,
"height": 1920,
"rotation": 0,
"codecs": "avc1.640028, mp4a.40.2",
"has_audio": true,
"frame_rate": 29.97,
"bitrate_kbps": 4200,
"faststart": true
},
"warnings": [
"NOT_FASTSTART"
],
"failure": {
"code": "VIDEO_HEVC",
"message": "This video is HEVC (H.265).",
"fix": "Export as H.264."
},
"created_at": "2026-08-11T10:00:00.000Z",
"completed_at": "2026-08-11T10:00:00.000Z",
"verified_at": "2026-08-11T10:00:00.000Z"
}
],
"meta": {
"has_more": true,
"next_cursor": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001"
}
}Start a video upload
/ext/v1/videosvideos:writeOpens an upload session for one H.264 MP4 of at most 200 MB (3 s to 5 min; AAC, MP3, Opus or no audio) and reserves its size against the plan's video storage. Then: ask POST /videos/{id}/part-urls for part URLs, PUT each part (exactly part_size bytes, the last one the rest) to its URL with the headers given, and call POST /videos/{id}/complete. Idempotency-Key is REQUIRED and is also the resume handle. A retry with the same key within 24 hours replays the first answer as it was (Idempotent-Replayed: true, resumed: false, its uploaded_parts as they were then); after that, the same key returns the same session with resumed: true. Either way the session is the same — read GET /videos/{id} for the parts storage holds now. Reuse a key only to retry the same file; once that session has ended (aborted), start over with a new key. A key made by rotating another keeps its uploads, and the same Idempotency-Key still names the same session. A finished video no product places within 48 hours is released (it stops counting against video storage) and removed 7 days later. At most 50 uploads start per store per day. Company-scoped keys must send store_id.
Headers
| Name | Type | Description |
|---|---|---|
Idempotency-Keyrequired | string | REQUIRED. A unique value per logical request (a UUID is ideal); reuse it verbatim when retrying. A retry replays the original response with Idempotent-Replayed: true instead of running twice. |
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. |
file_namerequired | string | For display only; never part of a storage key. |
size_bytesrequired | number | The exact size of the file in bytes — at most 200,000,000 (200 MB). It is reserved against the plan's video storage until the upload finishes or is cancelled. |
part_sizerequired | number | Bytes per part: 5, 8 or 16 MiB. Every part but the last is exactly this long; the answer says how many parts there are.5242880838860816777216 |
fingerprint | string | Optional. Hex sha256 of <size_bytes>: followed by the sha256 of the file's first MiB and of its last MiB. When sent, starting again with the same file (under a new Idempotency-Key) resumes the unfinished upload instead of opening another. |
product_id | uuid | The product the video is meant for, for an early check of its video limit. It does not place the video. |
client_probe | object | Optional. A client-side check of the file in the dashboard's ClientProbeReport shape (at most 32 KB as JSON); a file it already rejects is refused before any byte is sent. The server reads and verifies every file itself either way. |
Request
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS -X POST "https://api.shopdeskbd.com/ext/v1/videos" \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47" \
-H "Content-Type: application/json" \
-d '{
"file_name": "red-saree-drape.mp4",
"size_bytes": 48000000,
"part_size": 8388608
}'const url = new URL("https://api.shopdeskbd.com/ext/v1/videos");
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({
"file_name": "red-saree-drape.mp4",
"size_bytes": 48000000,
"part_size": 8388608
}),
});
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 = {
"file_name": "red-saree-drape.mp4",
"size_bytes": 48000000,
"part_size": 8388608,
}
response = requests.post(
"https://api.shopdeskbd.com/ext/v1/videos",
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([
'file_name' => 'red-saree-drape.mp4',
'size_bytes' => 48000000,
'part_size' => 8388608,
]);
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/videos');
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/videosStored 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 upload session, with its part plan.400The requiredIdempotency-Keyheader is missing, 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.403FORBIDDEN_SCOPE: the key's scopes do not cover this endpoint.PLAN_FEATURE_REQUIRED: the plan does not include product videos.VIDEO_UPLOADS_DISABLED: video uploads are switched off for now.PLAN_LIMIT_REACHED: the plan's video storage is full (orproduct_id’s product already has its plan’s number of videos).404product_idnames no product of this store.409TOO_MANY_OPEN_UPLOADS: this key already has 4 unfinished uploads, or the store 6 — complete or delete one first; or a request with thisIdempotency-Keyis still in flight (IDEMPOTENCY_IN_FLIGHT) — retry once it has finished.422Validation failed —error.fieldsnames the field. Withclient_probe: A file that breaks a rule gets its own code —VIDEO_NOT_MP4,VIDEO_HEVC,VIDEO_CODEC_UNSUPPORTED,VIDEO_PROFILE_UNSUPPORTED,VIDEO_AUDIO_UNSUPPORTED,VIDEO_FRAGMENTED,VIDEO_NO_VIDEO_TRACK,VIDEO_TOO_LARGE,VIDEO_TOO_LONG,VIDEO_TOO_SHORT,VIDEO_LOW_RES,VIDEO_FRAME_TOO_BIG,VIDEO_ROTATION_UNSUPPORTEDorVIDEO_CORRUPT— with how to fix it inerror.fields.video.429Rate limit exceeded for this key, or the store started 50 uploads today (RATE_LIMITED,Retry-Afterto the next UTC midnight).502STORAGE_UNAVAILABLE: video storage refused or was unreachable — retry in a moment.
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",
"status": "uploading",
"source": "upload",
"file_name": "red-saree-drape.mp4",
"size_bytes": 48000000,
"part_size": 8388608,
"part_count": 6,
"checksum_algorithm": "SHA256",
"uploaded_parts": [
{
"part_number": 1,
"size": 8388608,
"checksum_sha256": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="
}
],
"uploaded_bytes": 16777216,
"expires_at": "2026-08-11T10:00:00.000Z",
"product_ids": [],
"url": "https://cdn.example.com/companies/c/videos/v/video.mp4",
"poster_url": null,
"facts": {
"duration_ms": 24000,
"width": 1080,
"height": 1920,
"rotation": 0,
"codecs": "avc1.640028, mp4a.40.2",
"has_audio": true,
"frame_rate": 29.97,
"bitrate_kbps": 4200,
"faststart": true
},
"warnings": [
"NOT_FASTSTART"
],
"failure": {
"code": "VIDEO_HEVC",
"message": "This video is HEVC (H.265).",
"fix": "Export as H.264."
},
"created_at": "2026-08-11T10:00:00.000Z",
"completed_at": "2026-08-11T10:00:00.000Z",
"verified_at": "2026-08-11T10:00:00.000Z",
"resumed": false
}Get a video upload
/ext/v1/videos/{id}videos:writeThe upload and its status. While uploading, uploaded_parts lists the parts storage already holds — resume by sending the rest. Poll this after a 202 from complete until ready or rejected. A finished video no product places within 48 hours is released and, 7 days later, removed — then this answers 404.
Path parameters
| Name | Type | Description |
|---|---|---|
idrequired | uuid | The video id POST /videos returned. |
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/videos/REPLACE_WITH_ID" \
-H "Authorization: Bearer $SHOPDESK_API_KEY"const url = new URL("https://api.shopdeskbd.com/ext/v1/videos/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/videos/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/videos/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/videos/6b1f3c2d-9e8a-4b7c-a6d5-1e2f3a4b5c6dStored 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 upload.401Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.403FORBIDDEN_SCOPE: the key's scopes do not cover this endpoint.PLAN_FEATURE_REQUIRED: the plan does not include product videos.VIDEO_UPLOADS_DISABLED: video uploads are switched off for now.404No video with this id was started by this API key (another key’s or a dashboard upload is not visible here).422Validation failed —error.fieldsmaps each offending field to its messages.429Rate limit exceeded for this key. HonorRetry-Afterand theX-RateLimit-*headers.502STORAGE_UNAVAILABLE: video storage refused or was unreachable — retry in a moment.
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",
"status": "uploading",
"source": "upload",
"file_name": "red-saree-drape.mp4",
"size_bytes": 48000000,
"part_size": 8388608,
"part_count": 6,
"checksum_algorithm": "SHA256",
"uploaded_parts": [
{
"part_number": 1,
"size": 8388608,
"checksum_sha256": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="
}
],
"uploaded_bytes": 16777216,
"expires_at": "2026-08-11T10:00:00.000Z",
"product_ids": [],
"url": "https://cdn.example.com/companies/c/videos/v/video.mp4",
"poster_url": null,
"facts": {
"duration_ms": 24000,
"width": 1080,
"height": 1920,
"rotation": 0,
"codecs": "avc1.640028, mp4a.40.2",
"has_audio": true,
"frame_rate": 29.97,
"bitrate_kbps": 4200,
"faststart": true
},
"warnings": [
"NOT_FASTSTART"
],
"failure": {
"code": "VIDEO_HEVC",
"message": "This video is HEVC (H.265).",
"fix": "Export as H.264."
},
"created_at": "2026-08-11T10:00:00.000Z",
"completed_at": "2026-08-11T10:00:00.000Z",
"verified_at": "2026-08-11T10:00:00.000Z"
}Delete a video upload
/ext/v1/videos/{id}videos:writeCancels an unfinished upload (its reserved storage is released at once), or releases a finished video no live product shows: it stops counting against the plan's video storage now and is removed from storage after 7 days. A video only archived products show is taken off them. Deleting again changes nothing. A company-scoped key may narrow with the store_id query parameter.
Path parameters
| Name | Type | Description |
|---|---|---|
idrequired | uuid | The video id POST /videos returned. |
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/videos/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/videos/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/videos/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/videos/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/videos/6b1f3c2d-9e8a-4b7c-a6d5-1e2f3a4b5c6dStored 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.403FORBIDDEN_SCOPE: the key's scopes do not cover this endpoint.PLAN_FEATURE_REQUIRED: the plan does not include product videos.VIDEO_UPLOADS_DISABLED: video uploads are switched off for now.404No video with this id was started by this API key (another key’s or a dashboard upload is not visible here).409VIDEO_IN_USE: a live product shows the video — take it off the product first;UPLOAD_VERIFYING: the video is still being checked; 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.502STORAGE_UNAVAILABLE: video storage refused or was unreachable — retry in a moment.
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
}Complete a video upload
/ext/v1/videos/{id}/completevideos:writeCompletes the file from the parts storage holds, then the server reads and checks it. 200 with status: ready when the verdict is in; 202 with status: verifying while the check is still running — poll GET /videos/{id}. Calling it again is safe: a ready video answers 200 again, a rejected one 422 again. Send no Idempotency-Key on a repeat meant to re-check a video that stayed verifying: a replay answers the first result.
Path parameters
| Name | Type | Description |
|---|---|---|
idrequired | uuid | The video id POST /videos returned. |
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 POST "https://api.shopdeskbd.com/ext/v1/videos/REPLACE_WITH_ID/complete" \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47"const url = new URL("https://api.shopdeskbd.com/ext/v1/videos/REPLACE_WITH_ID/complete");
const response = await fetch(url, {
method: "POST",
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.post(
"https://api.shopdeskbd.com/ext/v1/videos/REPLACE_WITH_ID/complete",
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/videos/REPLACE_WITH_ID/complete');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
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/videos/6b1f3c2d-9e8a-4b7c-a6d5-1e2f3a4b5c6d/completeStored 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 video,ready.202The video, stillverifying.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.403FORBIDDEN_SCOPE: the key's scopes do not cover this endpoint.PLAN_FEATURE_REQUIRED: the plan does not include product videos.VIDEO_UPLOADS_DISABLED: video uploads are switched off for now.404No video with this id was started by this API key (another key’s or a dashboard upload is not visible here).409PARTS_INCOMPLETE: some parts are not in storage yet (error.fieldsnames them) — upload them and complete again;FILE_CHANGED: a stored part does not match its checksum; or a request with thisIdempotency-Keyis still in flight (IDEMPOTENCY_IN_FLIGHT) — retry once it has finished.410UPLOAD_NOT_ACTIVE: the upload expired or was cancelled — start a new one.422A file that breaks a rule gets its own code —VIDEO_NOT_MP4,VIDEO_HEVC,VIDEO_CODEC_UNSUPPORTED,VIDEO_PROFILE_UNSUPPORTED,VIDEO_AUDIO_UNSUPPORTED,VIDEO_FRAGMENTED,VIDEO_NO_VIDEO_TRACK,VIDEO_TOO_LARGE,VIDEO_TOO_LONG,VIDEO_TOO_SHORT,VIDEO_LOW_RES,VIDEO_FRAME_TOO_BIG,VIDEO_ROTATION_UNSUPPORTEDorVIDEO_CORRUPT— with how to fix it inerror.fields.video. The file is deleted and its storage released.429Rate limit exceeded for this key. HonorRetry-Afterand theX-RateLimit-*headers.502STORAGE_UNAVAILABLE: video storage refused or was unreachable — retry in a moment.
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",
"status": "uploading",
"source": "upload",
"file_name": "red-saree-drape.mp4",
"size_bytes": 48000000,
"part_size": 8388608,
"part_count": 6,
"checksum_algorithm": "SHA256",
"uploaded_parts": [
{
"part_number": 1,
"size": 8388608,
"checksum_sha256": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="
}
],
"uploaded_bytes": 16777216,
"expires_at": "2026-08-11T10:00:00.000Z",
"product_ids": [],
"url": "https://cdn.example.com/companies/c/videos/v/video.mp4",
"poster_url": null,
"facts": {
"duration_ms": 24000,
"width": 1080,
"height": 1920,
"rotation": 0,
"codecs": "avc1.640028, mp4a.40.2",
"has_audio": true,
"frame_rate": 29.97,
"bitrate_kbps": 4200,
"faststart": true
},
"warnings": [
"NOT_FASTSTART"
],
"failure": {
"code": "VIDEO_HEVC",
"message": "This video is HEVC (H.265).",
"fix": "Export as H.264."
},
"created_at": "2026-08-11T10:00:00.000Z",
"completed_at": "2026-08-11T10:00:00.000Z",
"verified_at": "2026-08-11T10:00:00.000Z"
}Get upload URLs for video parts
/ext/v1/videos/{id}/part-urlsvideos:writePresigned PUT URLs for up to 10 parts, each valid for 60 minutes. Send each part’s base64 SHA-256: it is signed into the URL (storage refuses other bytes) and pinned — the same part asked for again with a different checksum is 409 FILE_CHANGED. Ask again for fresh URLs whenever they expire — and send no Idempotency-Key here: asking again is always safe, and a replayed answer would hand back the first URLs, which may have expired.
Path parameters
| Name | Type | Description |
|---|---|---|
idrequired | uuid | The video id POST /videos returned. |
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. |
partsrequired | VideoPartUrlRequestDto[] | — |
parts[] fields
| Name | Type | Description |
|---|---|---|
part_numberrequired | number | — |
sha256required | string | Base64 SHA-256 of the part's bytes. It is signed into the URL, and the first checksum sent for a part is pinned: asking again with a different one is 409 FILE_CHANGED. |
Request
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS -X POST "https://api.shopdeskbd.com/ext/v1/videos/REPLACE_WITH_ID/part-urls" \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47" \
-H "Content-Type: application/json" \
-d '{
"parts": [
{
"part_number": 1,
"sha256": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="
}
]
}'const url = new URL("https://api.shopdeskbd.com/ext/v1/videos/REPLACE_WITH_ID/part-urls");
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({
"parts": [
{
"part_number": 1,
"sha256": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="
}
]
}),
});
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 = {
"parts": [
{
"part_number": 1,
"sha256": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=",
},
],
}
response = requests.post(
"https://api.shopdeskbd.com/ext/v1/videos/REPLACE_WITH_ID/part-urls",
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([
'parts' => [
[
'part_number' => 1,
'sha256' => 'n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=',
],
],
]);
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/videos/REPLACE_WITH_ID/part-urls');
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/videos/6b1f3c2d-9e8a-4b7c-a6d5-1e2f3a4b5c6d/part-urlsStored 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
200One URL per part asked for.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.403FORBIDDEN_SCOPE: the key's scopes do not cover this endpoint.PLAN_FEATURE_REQUIRED: the plan does not include product videos.VIDEO_UPLOADS_DISABLED: video uploads are switched off for now.404No video with this id was started by this API key (another key’s or a dashboard upload is not visible here).409FILE_CHANGED: a part was already signed with a different checksum — this is not the same file; start a new upload; or a request with thisIdempotency-Keyis still in flight (IDEMPOTENCY_IN_FLIGHT) — retry once it has finished.410UPLOAD_NOT_ACTIVE: the upload expired, was cancelled or already completed — start a new one.422Validation failed —error.fieldsmaps each offending field to its messages.429Rate limit exceeded for this key. HonorRetry-Afterand theX-RateLimit-*headers.502STORAGE_UNAVAILABLE: video storage refused or was unreachable — retry in a moment.
Every failure uses the one error envelope — Errors lists each code and what to do with it.
{
"data": [
{
"part_number": 1,
"url": "https://media.example.com/companies/c/videos/v/video.mp4?partNumber=1&uploadId=2a7f&X-Amz-Signature=9c1e",
"headers": {
"x-amz-checksum-sha256": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="
},
"content_length": 8388608,
"expires_at": "2026-08-11T10:00:00.000Z"
}
]
}Set a video cover
/ext/v1/videos/{id}/postervideos:writeSets the image shown before the video plays; with none, product pages show the product thumbnail. Three sources: **upload** — multipart/form-data with the image in a file part and source as a text field; **product_image** — JSON { "source": "product_image", "image_id": "…" } with one of a product’s photos (copied beside the video, so deleting the photo later keeps the cover); **url** — JSON { "source": "url", "url": "https://…" }, which Shopdesk downloads and stores (the link is not kept). The image must be a JPEG, PNG or WebP of at most 2 MB, read from its bytes. A cover may be set while the video is still uploading, and replaced any time; a video a product shows gets its new cover on the product page at once. Send each NEW cover with a new Idempotency-Key, or none: a key is matched on the form fields, not the file’s bytes, so reusing one with a different file replays the first answer. The answer is the video with its poster_url.
Path parameters
| Name | Type | Description |
|---|---|---|
idrequired | uuid | The video id POST /videos returned. |
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. |
sourcerequired | string | upload: a JPEG, PNG or WebP of at most 2 MB in a multipart file part. product_image: one of the product photos (image_id), copied beside the video. url: an https link to the image, which Shopdesk downloads and stores (the link is not kept).uploadproduct_imageurl |
image_id | uuid | product_image only: a photo of a product in this store, as listed in its images[]. |
url | string | url only: an https link to a JPEG, PNG or WebP image. |
Request
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS -X POST "https://api.shopdeskbd.com/ext/v1/videos/REPLACE_WITH_ID/poster" \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47" \
-H "Content-Type: application/json" \
-d '{
"source": "product_image",
"image_id": "9c1e4b2a-7d3f-4e58-b0a1-2f6d8c9e1a45"
}'const url = new URL("https://api.shopdeskbd.com/ext/v1/videos/REPLACE_WITH_ID/poster");
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({
"source": "product_image",
"image_id": "9c1e4b2a-7d3f-4e58-b0a1-2f6d8c9e1a45"
}),
});
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 = {
"source": "product_image",
"image_id": "9c1e4b2a-7d3f-4e58-b0a1-2f6d8c9e1a45",
}
response = requests.post(
"https://api.shopdeskbd.com/ext/v1/videos/REPLACE_WITH_ID/poster",
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([
'source' => 'product_image',
'image_id' => '9c1e4b2a-7d3f-4e58-b0a1-2f6d8c9e1a45',
]);
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/videos/REPLACE_WITH_ID/poster');
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/videos/6b1f3c2d-9e8a-4b7c-a6d5-1e2f3a4b5c6d/posterStored 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 video, with its newposter_url.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.403FORBIDDEN_SCOPE: the key's scopes do not cover this endpoint.PLAN_FEATURE_REQUIRED: the plan does not include product videos.VIDEO_UPLOADS_DISABLED: video uploads are switched off for now.404No video with this id was started by this API key (another key’s or a dashboard upload is not visible here).409A request with thisIdempotency-Keyis still in flight (IDEMPOTENCY_IN_FLIGHT) — retry once it has finished.410UPLOAD_NOT_ACTIVE: the upload was rejected, cancelled or expired — it takes no cover.413Thefilepart is larger than 8 MB.422No usable image —error.fieldsnamesfile,urlorimage_id: not a JPEG/PNG/WebP, over 2 MB, a link that cannot be downloaded, a photo id of no product in this store, or a missing or extrafilepart for the source.429Rate limit exceeded for this key. HonorRetry-Afterand theX-RateLimit-*headers.502STORAGE_UNAVAILABLE: video storage refused or was unreachable — retry in a moment.
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",
"status": "uploading",
"source": "upload",
"file_name": "red-saree-drape.mp4",
"size_bytes": 48000000,
"part_size": 8388608,
"part_count": 6,
"checksum_algorithm": "SHA256",
"uploaded_parts": [
{
"part_number": 1,
"size": 8388608,
"checksum_sha256": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="
}
],
"uploaded_bytes": 16777216,
"expires_at": "2026-08-11T10:00:00.000Z",
"product_ids": [],
"url": "https://cdn.example.com/companies/c/videos/v/video.mp4",
"poster_url": null,
"facts": {
"duration_ms": 24000,
"width": 1080,
"height": 1920,
"rotation": 0,
"codecs": "avc1.640028, mp4a.40.2",
"has_audio": true,
"frame_rate": 29.97,
"bitrate_kbps": 4200,
"faststart": true
},
"warnings": [
"NOT_FASTSTART"
],
"failure": {
"code": "VIDEO_HEVC",
"message": "This video is HEVC (H.265).",
"fix": "Export as H.264."
},
"created_at": "2026-08-11T10:00:00.000Z",
"completed_at": "2026-08-11T10:00:00.000Z",
"verified_at": "2026-08-11T10:00:00.000Z"
}Import a video from a link
/ext/v1/videos/importvideos:writeShopdesk downloads an H.264 MP4 from an https link to the file itself (not a player page such as YouTube) and stores its own copy — the link is not kept. Always answered 202 at once: the copy runs in the background, so poll GET /videos/{id} until ready or rejected (or aborted with an import_* failure code). The link must be public (so must any of the up to 3 redirects it takes) and must declare its size (Content-Length): an import is refused before its body is downloaded when that is over 200 MB or over the plan's remaining video storage, which is reserved from the start and released if the import fails; a download longer or shorter than declared is refused too. Nothing stays in storage from a failed import. The file is then verified exactly like an upload. Imports run one at a time per company; one whose copy has not started within an hour ends aborted (expired), and one whose link sends under 64 KB a second on average after its first minute ends import_download_failed. If the key is revoked or expires, or the plan loses product videos, before the copy starts, the import ends aborted (cancelled). Without file_name the video is named after the link’s last path segment when that reads as a file name (red-saree.mp4), else video.mp4. Idempotency-Key is REQUIRED and names the import: within 24 hours a retry replays the first answer; after that the same key returns the same video (resumed: true) — and a retry is never refused by the daily cap. At most 20 imports start per key per day, within the store’s 50 uploads a day. Company-scoped keys must send store_id.
Headers
| Name | Type | Description |
|---|---|---|
Idempotency-Keyrequired | string | REQUIRED. A unique value per logical request (a UUID is ideal); reuse it verbatim when retrying. A retry replays the original response with Idempotent-Replayed: true instead of running twice. |
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. |
source_urlrequired | string | An https link to the MP4 file itself (not a player page such as YouTube). Shopdesk downloads it and stores its own copy; the link is not kept. |
file_name | string | For display only. Defaults to the link's last path segment. |
product_id | uuid | The product the video is meant for, for an early check of its video limit. It does not place the video — POST /products/{id}/videos does. |
Request
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS -X POST "https://api.shopdeskbd.com/ext/v1/videos/import" \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: 3f2b8c1e-9d4a-4e7b-8a6f-2c5d1e0b9a47" \
-H "Content-Type: application/json" \
-d '{
"source_url": "https://cdn.example.com/videos/red-saree-drape.mp4"
}'const url = new URL("https://api.shopdeskbd.com/ext/v1/videos/import");
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({
"source_url": "https://cdn.example.com/videos/red-saree-drape.mp4"
}),
});
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 = {
"source_url": "https://cdn.example.com/videos/red-saree-drape.mp4",
}
response = requests.post(
"https://api.shopdeskbd.com/ext/v1/videos/import",
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([
'source_url' => 'https://cdn.example.com/videos/red-saree-drape.mp4',
]);
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/videos/import');
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/videos/importStored 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
202The video,source: import, stilluploadingwhile it is copied.400The requiredIdempotency-Keyheader is missing, 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.403FORBIDDEN_SCOPE: the key's scopes do not cover this endpoint.PLAN_FEATURE_REQUIRED: the plan does not include product videos.VIDEO_UPLOADS_DISABLED: video uploads are switched off for now.PLAN_LIMIT_REACHED: the plan's video storage is full (orproduct_id’s product already has its plan’s number of videos).404product_idnames no product of this store.409TOO_MANY_OPEN_UPLOADS: this key already has 4 unfinished uploads or imports, or the store 6 — wait for one to finish or delete one; or a request with thisIdempotency-Keyis still in flight (IDEMPOTENCY_IN_FLIGHT) — retry once it has finished.422Validation failed —error.fieldsnames the field.source_urlmust be an https link without a username or password, on port 443 or from 1024 up, and not a video player page.429Rate limit exceeded for this key, or this key started 20 imports (or the store 50 uploads) today (RATE_LIMITED,Retry-Afterto the next UTC midnight).502STORAGE_UNAVAILABLE: video storage refused or was unreachable — retry in a moment.
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",
"status": "uploading",
"source": "upload",
"file_name": "red-saree-drape.mp4",
"size_bytes": 48000000,
"part_size": 8388608,
"part_count": 6,
"checksum_algorithm": "SHA256",
"uploaded_parts": [
{
"part_number": 1,
"size": 8388608,
"checksum_sha256": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="
}
],
"uploaded_bytes": 16777216,
"expires_at": "2026-08-11T10:00:00.000Z",
"product_ids": [],
"url": "https://cdn.example.com/companies/c/videos/v/video.mp4",
"poster_url": null,
"facts": {
"duration_ms": 24000,
"width": 1080,
"height": 1920,
"rotation": 0,
"codecs": "avc1.640028, mp4a.40.2",
"has_audio": true,
"frame_rate": 29.97,
"bitrate_kbps": 4200,
"faststart": true
},
"warnings": [
"NOT_FASTSTART"
],
"failure": {
"code": "VIDEO_HEVC",
"message": "This video is HEVC (H.265).",
"fix": "Export as H.264."
},
"created_at": "2026-08-11T10:00:00.000Z",
"completed_at": "2026-08-11T10:00:00.000Z",
"verified_at": "2026-08-11T10:00:00.000Z",
"resumed": false
}