Errors
The Slicer API error format, HTTP statuses and stable error codes, which errors are safe to retry and how to handle them in code.
Every API error comes with an HTTP status and a body of the same shape. Make decisions in code by the code field:
codes are stable within v1, while message is written for people — in the account language — and may be reworded.
{
"ok": false,
"error": {
"code": "insufficient_scope",
"message": "The token lacks access: Run montage — spends tokens (montage:run)"
}
}Error
| Field | Type | Description |
|---|---|---|
okrequired | false | |
errorrequired | object | |
error.coderequired | string | Stable snake_case error code |
error.messagerequired | string | Human-readable message in the account language |
error.fields | Record<string, string> | Field errors |
error.retryAfterSec | integer | Seconds to wait before retrying |
fields— per-field errors when the request body fails validation:{ "uploadIds": "…" }.retryAfterSec— how many seconds to wait before retrying (when a rate limit is exceeded).
Statuses
| Status | When | What to do |
|---|---|---|
| 400 | the body or parameters failed validation, or the file is not acceptable | fix the request; details are in fields and message |
| 401 | no token, or the token is wrong, revoked or expired | check the token; for token_expired and token_revoked, issue a new one |
| 402 | not enough tokens on the balance, or the token's spend limit is used up | top up in the app or raise the token's limit |
| 403 | the token lacks the required scope | issue a token with the right scopes |
| 404 | the resource doesn't exist or belongs to another account | check the id; other people's resources don't exist for the API |
| 409 | a state conflict: the price changed, the document is stale, the project is busy | re-read the state and retry |
| 410 | the upload link has expired or was already used | create a new upload |
| 429 | rate limit exceeded | wait Retry-After seconds |
| 5xx | a temporary server or AI failure | retry later with the same Idempotency-Key |
Common codes
| Code | Status | Meaning |
|---|---|---|
unauthorized | 401 | no Authorization: Bearer header |
invalid_token | 401 | token not found or mistyped |
token_revoked | 401 | the token was revoked, the app disconnected or the account deleted |
token_expired | 401 | the token has expired |
insufficient_scope | 403 | the token lacks a scope; the missing ones are listed in message |
not_found | 404 | project, upload or webhook not found |
insufficient_tokens | 402 | the balance is lower than the operation's price |
spend_limit_exceeded | 402 | the operation doesn't fit into the token's monthly spend limit |
cost_changed | 409 | confirmCost doesn't match the current price — request the price again |
stale_document | 409 | the document revision is outdated — re-read rev and repeat the edit |
busy | 409 | the project is already being worked on (montage, render) — wait for it to finish |
not_ready | 409 | the video isn't ready for this operation yet |
too_many_drafts | 409 | too many drafts of projects without the AI director — export or delete some |
daily_export_limit | 429 | today's free exports of projects without the AI director are used up — retry after retryAfterSec |
manual_disabled | 503 | projects without the AI director are temporarily unavailable |
request_in_progress | 409 | a request with this Idempotency-Key is still running |
bad_idempotency_key | 409 | Idempotency-Key has the wrong format: 8–128 characters A–Z a–z 0–9 _ - |
bad_cursor | 400 | the page cursor is damaged — start from the first page |
platform_requires_app | 400 | publishing to this platform is only possible from the Slicer app |
rate_limit_exceeded | 429 | too many requests per minute for the token |
ai_unavailable | 503 | AI is temporarily unavailable — retry later, no tokens were charged |
api_disabled | 503 | the public API is temporarily turned off |
internal_server_error | 500 | an unexpected error — retry later |
Upload errors (unsupported_format, file_too_large, resolution_too_low, link_expired and others) are described in
Uploading files.
All codes
| Code | HTTP | When |
|---|---|---|
unauthorized | 401 | No token, or the token is not valid |
token_revoked | 401 | The token was revoked or the app was disconnected |
token_expired | 401 | The token has expired |
insufficient_scope | 403 | The token lacks a required scope (see WWW-Authenticate) |
forbidden | 403 | The action is not allowed for this account |
not_found | 404 | No such object, or it belongs to another account |
bad_request | 400 | Invalid request; field errors are in fields |
conflict | 409 | The object is in a state that does not allow the action |
stale_document | 409 | The document has changed: fetch the latest rev and retry |
busy | 409 | The project is busy with a running job |
not_ready | 409 | The video or upload is not ready yet |
request_in_progress | 409 | A request with this Idempotency-Key is still running |
cost_changed | 409 | The price has changed: confirm the new cost |
insufficient_tokens | 402 | Not enough tokens on the balance |
spend_limit_exceeded | 402 | The token's monthly spend limit is reached |
rate_limit_exceeded | 429 | Too many requests: wait Retry-After seconds |
file_too_large | 400 | The file is larger than allowed for its type |
unsupported_format | 400 | The file format is not supported |
empty_file | 400 | The file is empty |
parts_missing | 400 | Not all parts were uploaded before complete |
upload_not_active | 409 | The upload is already completed or cancelled |
link_expired | 410 | The one-time upload link has expired |
link_used | 410 | The one-time upload link was already used |
too_many_imports | 429 | Too many imports by URL are running at once |
platform_requires_app | 400 | Publishing to this platform is only available in the Slicer app |
ai_unavailable | 503 | The AI service is temporarily unavailable: retry later |
What to retry
Retrying with the same Idempotency-Key is safe: if the first request reached the server, you get its response
instead of a second operation (Idempotency).
- Network failure, 500, 502, 503, 504 — retry with exponential backoff: 1, 2, 4, 8 seconds and so on, 5–6 attempts at most.
- 429 — wait as many seconds as
Retry-After(orretryAfterSec) says, then retry. - 409
request_in_progress— the first request is still running; wait a couple of seconds and retry with the same key. - 409
cost_changed,stale_document— don't retry blindly: re-read the price or the document, show it to the user if needed, and send the request again. - 400, 401, 402, 403, 404 — retrying won't help; fix the request, the token or the balance.
import { randomUUID } from 'node:crypto';
export class SlicerError extends Error {
constructor(status, error) {
super(`${status} ${error.code}: ${error.message}`);
this.status = status;
this.code = error.code;
this.fields = error.fields;
this.retryAfterSec = error.retryAfterSec;
}
}
const RETRYABLE = new Set([429, 500, 502, 503, 504]);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/** API request with retries: one Idempotency-Key for all attempts. */
export async function slicer(method, path, body, { attempts = 5 } = {}) {
const key = method === 'POST' ? randomUUID() : undefined;
for (let attempt = 1; ; attempt += 1) {
let response;
try {
response = await fetch(`https://api.slicer.media/v1${path}`, {
method,
headers: {
Authorization: `Bearer ${process.env.SLICER_TOKEN}`,
...(body ? { 'Content-Type': 'application/json' } : {}),
...(key ? { 'Idempotency-Key': key } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
} catch (networkError) {
if (attempt >= attempts) throw networkError;
await sleep(2 ** attempt * 500);
continue;
}
const json = await response.json();
if (json.ok) return json.data;
const retry = RETRYABLE.has(response.status) || json.error.code === 'request_in_progress';
if (!retry || attempt >= attempts) throw new SlicerError(response.status, json.error);
const wait = Number(response.headers.get('Retry-After')) || 2 ** attempt * 0.5;
await sleep(wait * 1000);
}
}import os
import time
import uuid
import requests
RETRYABLE = {429, 500, 502, 503, 504}
class SlicerError(Exception):
def __init__(self, status, error):
super().__init__(f"{status} {error['code']}: {error['message']}")
self.status = status
self.code = error["code"]
self.fields = error.get("fields")
def slicer(method, path, json=None, attempts=5):
"""API request with retries: one Idempotency-Key for all attempts."""
headers = {"Authorization": f"Bearer {os.environ['SLICER_TOKEN']}"}
if method == "POST":
headers["Idempotency-Key"] = str(uuid.uuid4())
for attempt in range(1, attempts + 1):
try:
response = requests.request(
method, f"https://api.slicer.media/v1{path}", json=json, headers=headers, timeout=60
)
except requests.ConnectionError:
if attempt == attempts:
raise
time.sleep(2 ** attempt * 0.5)
continue
body = response.json()
if body["ok"]:
return body["data"]
error = body["error"]
retry = response.status_code in RETRYABLE or error["code"] == "request_in_progress"
if not retry or attempt == attempts:
raise SlicerError(response.status_code, error)
time.sleep(float(response.headers.get("Retry-After") or 2 ** attempt * 0.5))