# 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.

Source: https://docs.slicer.media/en/api/errors/

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.

Error response:

```json
{
  "ok": false,
  "error": {
    "code": "insufficient_scope",
    "message": "The token lacks access: Run montage — spends tokens (montage:run)"
  }
}
```

`Error`

| Field | Type | Description |
| --- | --- | --- |
| `ok` _(required)_ | `false` |     |
| `error` _(required)_ | `object` |     |
| `error.code` _(required)_ | `string` | Stable snake\_case error code |
| `error.message` _(required)_ | `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](https://docs.slicer.media/en/api/montage/#manual) — 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](https://docs.slicer.media/en/api/uploads/).

## 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](https://docs.slicer.media/en/api/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` (or `retryAfterSec`) 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.

**JavaScript**

slicer.mjs:

```js
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);
  }
}
```

**Python**

slicer.py:

```python
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))
```

## What's next

[IdempotencyHow to retry requests safely.](https://docs.slicer.media/en/api/idempotency/)[Rate limitsRequests per minute and X-RateLimit headers.](https://docs.slicer.media/en/api/rate-limits/)
