Every endpoint of the unshackle REST API reports failures in a single, predictable
shape. Whether a request was rejected because a field was missing, a service refused
to authenticate, or the download worker crashed, the server answers with a JSON body
that carries a machine-readable **error code**, a human-readable **message**, and a
matching **HTTP status**. This page documents that format and catalogues every error
code the server can emit.
If you are building a client against the API, read this page alongside the endpoint
reference. Each endpoint lists the specific codes it raises, and the meanings of
those codes are defined here.
!!! note "Where this comes from"
The error format is defined in `unshackle/core/api/errors.py`, and the wrapper
that turns raised errors into responses lives in `unshackle/core/api/routes.py`.
Nothing on this page is invented; it reflects the actual server behaviour.
## The standard error shape
A structured error is always a JSON object with `status` set to the literal string
`"error"`. The full set of fields:
```json title="Error response body"
{
"status": "error",
"error_code": "INVALID_PARAMETERS",
"message": "vcodec must be one of: H264, H265, ...",
"timestamp": "2026-07-03T12:34:56.789012+00:00",
"details": { "field": "vcodec" },
"retryable": true,
"debug_info": {
"exception_type": "APIError",
"traceback": "..."
}
}
```
| Field | Type | Always present? | Meaning |
|---|---|---|---|
| `status` | string | Yes | Always the literal `"error"`. Lets a client distinguish an error body from a success body at a glance. |
| `error_code` | string | Yes | A stable, machine-readable code from the [catalogue below](#error-code-catalogue). Match on this, not on `message`. |
| `message` | string | Yes | A human-readable description of what went wrong. Wording may change between versions, so do not parse it. |
| `timestamp` | string | Yes | ISO 8601 timestamp (UTC) of when the error response was built. |
| `details` | object | Only when non-empty | Structured, error-specific context (for example the invalid field, a list of available titles, or the set of currently active jobs). |
| `retryable` | boolean | Only when `true` | Present and set to `true` when retrying the same request may succeed later (network blips, rate limiting, transient outages). Absent otherwise. |
| `debug_info` | object | Only with `--debug-api` | Technical diagnostics; see [Debug information](#debug-information). |
!!! tip "Match on `error_code`, branch on HTTP status"
Treat `error_code` as the primary signal your code reacts to, and the HTTP status
as a coarse fallback for anything you do not recognise. The two are consistent:
every code maps to a default status (see the catalogue), and the pairing is
stable across releases.
### The `details` object
When present, `details` carries extra context specific to the error. It is only
included when it is non-empty, so a client must treat a missing `details` as "no
extra information". Some concrete examples you may encounter:
- A validation failure may include the offending field or the list of valid values.
- A "title not found" error on a remote-dl session includes `available_titles`.
- A maintenance conflict includes `active_jobs`, the jobs blocking the operation.
- Categorised exceptions add a `reason` tag, such as `"network_connectivity"` or
`"geofence_restriction"`.
### The `retryable` hint
The `retryable` field appears **only when it is `true`**. It is set for conditions
the server believes are transient: network errors, rate limiting, and temporary
service unavailability. When you see it, backing off and retrying is reasonable; when
it is absent, assume the request will keep failing until you change something.
Every code below is a member of the `APIErrorCode` enumeration. The **Status** column
is the default HTTP status the server returns for that code. A handler can override
the status in special cases (for example, a network error is always forced to `503`),
but the defaults hold for the overwhelming majority of responses.
### Client errors (4xx)
| Code | Status | Meaning |
|---|---|---|
| `INVALID_INPUT` | 400 | A required field is missing, or the request body could not be parsed. Also used as the generic "your input was malformed" code. |
| `INVALID_SERVICE` | 400 | The named service is unknown, or is not permitted for your API key. |
| `INVALID_PROXY` | 400 | The supplied proxy specification could not be resolved or is malformed. |
| `INVALID_PARAMETERS` | 400 | One or more download or query parameters failed validation (bad codec, bitrate, sort field, and so on). |
| `AUTH_FAILED` | 401 | Authentication with the streaming service failed (bad credentials or cookies). |
| `FORBIDDEN` | 403 | The action is not allowed. Raised by server-side gates (per-key restrictions on CDM or credential overrides) and by session IP binding. |
| `GEOFENCE` | 403 | The content is not available in the applicable region. |
| `NOT_FOUND` | 404 | A requested resource (title, history entry, and so on) does not exist. |
| `NO_CONTENT` | 404 | The request was valid but produced nothing: no matching titles, tracks, episodes, or keys. |
| `JOB_NOT_FOUND` | 404 | The referenced download job does not exist. |
| `SESSION_NOT_FOUND` | 404 | The remote-dl session does not exist or has expired. |
| `TRACK_NOT_FOUND` | 404 | A referenced track ID is not present in the session. |
| `CONFLICT` | 409 | The target resource is in a state that disallows the action (retrying a running job, prioritising a non-queued job, clearing cache during an active download). |
| `RATE_LIMITED` | 429 | The streaming service is rate-limiting requests. Retryable. |
### Server errors (5xx)
| Code | Status | Meaning |
|---|---|---|
| `INTERNAL_ERROR` | 500 | An unexpected server-side error. This is the fallback for any exception the server could not categorise. |
| `DOWNLOAD_ERROR` | 500 | The download process itself failed. |