# Errors

> **TL;DR** — Branch on `error_code` (creatives: the numeric `code`), never on the message. Keep a default branch: an unknown code is handled by its HTTP status class (`429` and `5xx` retry, other `4xx` do not). This page lists every status and code each endpoint returns, with what your app shows and what your backend does. When to retry, and how: the [retry policy](https://docs.wandura.ai/developers/reference/behaviour#retries).

## Error formats
### Flat format
Catalog, playback, configuration and open video use a flat object:

```json
{
  "error": "Episode not found or not ready to stream",
  "error_code": "not_ready"
}
```

| Field | Type | Description |
|---|---|---|
| `error` | string | A message for your logs. Do not show it to viewers and do not parse it, except where a row below says the code is in `error`. |
| `error_code` | string | The machine-readable code. Branch on it. Absent on a few errors (shown as **none** below). |
| `reason` | string | With `not_entitled`, and with `429` `prefetch_shed`: the detail. |
| `fields` | array of strings | With `invalid_device_info`: the device values that were rejected. |
| `protection` | string | With `sdk_required`: the protection level of the episode. |
| `supported_versions`, `latest_version`, `upgrade_to` |  | With `unsupported_version`: what you can send instead. |

A few `4xx` answers carry no `error_code` and put a stable code in `error` instead: `org_suspended`, `app_not_registered`, `collection_not_found` and `video_not_found`. Compare the whole string.

### Creatives envelope
The creatives endpoints wrap errors in an envelope with a numeric `code`. Errors have **no `data`** field:

```json
{
  "code": 4011,
  "message": "Invalid or revoked API key",
  "server_time": 1790679032
}
```

`code` is four digits: the first three are the HTTP status. Two creatives answers use the **flat** format instead: `429` `rate_limited` and `400` `unsupported_version`.

### Request id
Every response carries a `Wandura-Request-Id` header (for example `req_01J9Z3W2N8V4K7D5QX6T0B1C2M`), and every JSON error body, flat or envelope, a `request_id` field with the same value. Log it, and quote it when you contact support: it finds the exact request in Wandura's logs.

## Handle errors by code
| You received | Do |
|---|---|
| A code listed on this page | Follow its row. |
| An unknown `error_code` with `429` | Wait `Retry-After`, then retry. |
| An unknown `error_code` with another `4xx` | Do not retry. Show a generic error and alert your engineers. |
| An unknown `error_code` with a `5xx` | Retry with backoff, like `503`. |
| No body, or a body that is not JSON | Decide by status class, the same way. |

New codes can appear within `v1` at any time; an app without a default branch shows a blank screen when one does. Never show `error` or `message` to viewers: map codes to your own text.

### `Retry-After`
Sent with every `429`, and with the `503`s marked "after `Retry-After`" below. Never retry sooner. `503` `not_ready`, creatives `503`s, `502` and `500` carry none: use backoff.

## Messages for viewers
The "Your app shows" column below uses these four messages. Use your own wording.

| Message | When |
|---|---|
| **Unavailable** — "This title is no longer available" | The licence is not active. |
| **Not yet** — "Not available yet" | The episode is not playable right now. |
| **Busy** — "Busy, try again in a moment" | Retried and still failing temporarily. |
| **Broken** — "Couldn't load this episode" | A problem in your integration. Alert your engineers. |

## Every endpoint
### Every path
| Status | Code | When | Retry? | Do |
|---|---|---|---|---|
| 404 | `not_found` | Unknown path under `/api/delivery/v1/`, any method. No `Wandura-Version` header. | No | Fix the path. |
| 405 | none (empty body) | A real path with the wrong method, for example `GET /playback`. The `Allow` header lists the right one. | No | Fix the method. |

### Catalog
`GET /api/delivery/v1/catalog`

| Status | Code | When | Retry? | Your app shows | Your backend does |
|---|---|---|---|---|---|
| 400 | `invalid_request` | `limit` is not 1–200, or `cursor` was changed. | No | — | Fix the request. Restart the walk from page one. |
| 400 | `unsupported_version` | The `Wandura-Version` header, or your key's pin, is unknown or retired. | No | — | Send a supported version; see [Migrating versions](https://docs.wandura.ai/developers/guides/migrating-versions). |
| 401 | none | Header missing, or key unknown or revoked. | No | — | Check the key. Keep your previous catalog. |
| 403 | none | The key lacks `metadata:read`. | No | — | Use a key with that scope. |
| 403 | `org_suspended` (in `error`) | Your organisation is suspended. | No | — | Keep your copy, stop selling, contact Wandura. |
| 429 | `rate_limited` | Catalog limit reached. | After `Retry-After` | — | Wait, then find the loop. |
| 503 | `service_unavailable` | Wandura could not check the key in time. The key is fine. | After `Retry-After` | — | Retry. Never rotate the key for this. |
| 503 | `service_unavailable` | Wandura could not read the catalog in time (`Could not read the catalog in time — retry`). | After `Retry-After` | — | Retry. **Never** treat it as an empty catalog. |

### List series
`GET /api/delivery/v1/series`

| Status | Code | When | Retry? | Your backend does |
|---|---|---|---|---|
| 400 | `unsupported_version` (flat) | Unknown or retired dated version. | No | Send a supported version. |
| 401 | `4010` | No `Authorization: Bearer` header. | No | Fix the request. |
| 401 | `4011` | Key unknown or revoked. | No | Check the key. |
| 403 | `4030` | The key lacks `metadata:read`. | No | Use a key with that scope. |
| 403 | `4031` | Your organisation is suspended. | No | Contact Wandura. |
| 422 | `4220` | `limit` is not 1–100, `updated_since` is not epoch seconds, or `cursor` is not a value this endpoint returned. | No | Fix the request; restart from your last checkpoint. |
| 429 | `rate_limited` (flat) | Catalog limit reached (shared with the catalog). | After `Retry-After` | Wait; sync less often. |
| 503 | `5030` | The list is temporarily unavailable. | Backoff | Retry; keep your checkpoint. |
| 503 | `5031` | Wandura could not check the key in time. | Backoff | Retry. Do not rotate the key. |

### Get series metadata
`GET /api/delivery/v1/series/{code}`

| Status | Code | When | Retry? | Your backend does |
|---|---|---|---|---|
| 400 | `unsupported_version` (flat) | Unknown or retired dated version. | No | Send a supported version. |
| 401 | `4010` | No `Authorization: Bearer` header. | No | Fix the request. |
| 401 | `4011` | Key unknown or revoked. | No | Check the key. |
| 403 | `4030` | The key lacks `metadata:read`. | No | Use a key with that scope. |
| 403 | `4031` | Your organisation is suspended. | No | Contact Wandura. |
| 404 | `4040` | No such series, or your organisation may not stream it (the same answer on purpose). | No | Stop showing it; check the list. |
| 429 | `rate_limited` (flat) | Catalog limit reached (shared with the catalog). | After `Retry-After` | Wait. |
| 503 | `5030` | The series is temporarily unavailable. | Backoff | Retry. |
| 503 | `5031` | Wandura could not check the key in time. | Backoff | Retry. |

### Playback
`POST /api/delivery/v1/playback`. In a batch, the request-level rows answer the whole request; the per-ticket rows come back inside `items[]`, each with its own `status` (and `retry_after` instead of the header).

**Whole request**

| Status | Code | When | Retry? | Your app shows | Your backend does |
|---|---|---|---|---|---|
| 400 | none | The body is not JSON, or has neither `episode_id` nor `film_ref` with a numeric `episode_no`. | No | Broken | Fix the request. |
| 400 | `invalid_request` | Only one of `union_info` and `drm_kek`, or a malformed batch. | No | Broken | Fix the request. |
| 400 | `invalid_device_info` | A device value is empty-looking, for example `"(null)"`, or under 8 characters. `fields` names it. | No | Broken | Fix how the app reads the Player SDK values. Alert. |
| 400 | `unsupported_version` | Unknown or retired dated version. | No | Broken | See [Migrating versions](https://docs.wandura.ai/developers/guides/migrating-versions). |
| 401 | none | Header missing, or key unknown or revoked. | No | Broken | Check the key. Alert. |
| 403 | none | The key lacks `playback`. | No | Broken | Use a playback key. |
| 429 | `rate_limited` | Playback limit reached for every item. With `"reason": "prefetch_shed"`: prefetch is paused near the limit. | After `Retry-After` | Busy (play); nothing (prefetch) | Retry plays; drop prefetches. See [Rate limits](https://docs.wandura.ai/developers/concepts/rate-limits). |
| 503 | `service_unavailable` | Wandura could not check the key in time. | After `Retry-After` | Busy | Retry. Do not rotate the key. |
| 500 | none (rare) | An unexpected error. The JSON body carries `request_id`. | Backoff | Busy | Retry once; report it with the request id. |

**Per ticket** (top level for a single episode, or one item of a batch)

| Status | Code | When | Retry? | Your app shows | Your backend does |
|---|---|---|---|---|---|
| 403 | `not_entitled` | Your organisation may not stream this film right now. `reason` says why (table below). | No | Unavailable | Log `reason`. Stop selling the film once your next catalog sync confirms it is gone. |
| 404 | `not_ready` | The episode or film is unknown, not published or not ready. | No | Not yet | Refresh your catalog. |
| 409 | `sdk_required` | Encrypted episode and no device values. | No | Broken | Send `union_info` and `drm_kek`. Alert. |
| 429 | `rate_limited` | This item could not be admitted (batch only). `reason` `prefetch_shed` for a paused prefetch. | After `retry_after` | Nothing for a prefetch | Retry this item only. |
| 502 | none | The stream could not be prepared, or the play token could not be signed. | Backoff | Busy | Retry, then report. |
| 503 | `not_ready` | No playable version of the episode right now. No `Retry-After`. | Not now | Not yet | Try again later (next play or sync). |
| 503 | `service_unavailable` | The video provider did not answer in time (`The video provider did not answer in time — retry`). | After `Retry-After` (2 s) | Busy | Retry. The episode is not missing. |
| 503 | `service_unavailable` | The ticket could not be issued right now (`Could not issue this grant right now — retry`). | After `Retry-After` | Busy | Retry. |

> **Do not take irreversible steps on one answer.** Before you stop selling a film or refund viewers because of `not_entitled` or `not_ready`, confirm it with your next catalog sync: a withdrawn film disappears from the catalog.

If Wandura cannot check your licence or the episode in time while issuing a ticket, it answers `503` `service_unavailable` (`Could not check the licence or episode right now — retry`) with `Retry-After`, never `404` or `403`. Keep selling, and retry.

#### `not_entitled` reasons
Log `reason` and quote it to support, but show the same screen for all of them: new reasons may be added.

| `reason` | Meaning |
|---|---|
| `org_suspended` | Your organisation's access is suspended. |
| `entitlement_not_found` | Your organisation has no licence for this film. |
| `entitlement_suspended` | Your licence for this film is suspended. |
| `streaming_disabled` | Your licence for this film does not include streaming. |
| `outside_window` | Today (UTC) is outside your licence window. |

### Configuration
`GET /api/delivery/v1/config`

| Status | Code | When | Retry? | Your backend does |
|---|---|---|---|---|
| 400 | `unsupported_version` | Unknown or retired dated version. | No | Send a supported version. |
| 401 | none | Header missing, or key unknown or revoked. | No | Check the key. |
| 403 | `org_suspended` (in `error`) | Your organisation is suspended. | No | Contact Wandura. |
| 429 | `rate_limited` | Configuration limit reached. | After `Retry-After` | Read it less often: at start-up or every few hours. |
| 503 | `service_unavailable` | Wandura could not check the key in time. | After `Retry-After` | Retry; keep the configuration you had. |
| 503 | `service_unavailable` | Wandura could not read your configuration in time (`Could not read your configuration in time — retry`). | After `Retry-After` | Retry; keep the configuration you had. |

### List creatives
`GET /api/delivery/v1/creatives`

| Status | Code | When | Retry? | Your backend does |
|---|---|---|---|---|
| 400 | `unsupported_version` (flat) | Unknown or retired dated version. | No | Send a supported version. |
| 401 | `4010` | No `Authorization: Bearer` header. | No | Fix the request. |
| 401 | `4011` | Key unknown or revoked. | No | Check the key. |
| 403 | `4030` | The key lacks `creatives:export`. | No | Use a key with that scope. |
| 403 | `4031` | Your organisation is suspended. | No | Contact Wandura. |
| 422 | `4220` | `limit` is not 1–100 (an empty `limit=` counts as 0). | No | Fix the request. |
| 422 | `4221` | `updated_since` is not epoch seconds. | No | Fix the request. |
| 422 | `4222` | `cursor` is not valid. | No | Restart from your last checkpoint. |
| 429 | `rate_limited` (flat) | Creatives limit reached. | After `Retry-After` | Wait; sync less often. |
| 503 | `5030` | The feed is temporarily unavailable. | Backoff | Retry; keep your checkpoint. |
| 503 | `5031` | Wandura could not check the key in time. | Backoff | Retry. Do not rotate the key. |

### Get a creative
`GET /api/delivery/v1/creatives/{id}`

| Status | Code | When | Retry? | Your backend does |
|---|---|---|---|---|
| 400 | `unsupported_version` (flat) | Unknown or retired dated version. | No | Send a supported version. |
| 401 | `4010` | No `Authorization: Bearer` header. | No | Fix the request. |
| 401 | `4011` | Key unknown or revoked. | No | Check the key. |
| 403 | `4030` | The key lacks `creatives:export`. | No | Use a key with that scope. |
| 403 | `4031` | Your organisation is suspended. | No | Contact Wandura. |
| 404 | `4040` | No approved creative with this ID for your organisation (also: the ID is not a UUID). | No | Remove it from your active set. |
| 429 | `rate_limited` (flat) | Creatives limit reached (shared with the list). | After `Retry-After` | Wait. |
| 503 | `5030` | The feed is temporarily unavailable. | Backoff | Retry. |
| 503 | `5031` | Wandura could not check the key in time. | Backoff | Retry. |

### List open videos
`GET /api/delivery/v1/videos`

| Status | Code | When | Retry? | Your backend does |
|---|---|---|---|---|
| 400 | none | `cursor` is not a token this endpoint issued, or `limit` is not 1–200. | No | Fix the request; restart the sweep. |
| 400 | `unsupported_version` | Unknown or retired dated version. | No | Send a supported version. |
| 401 | none | Header missing, or key unknown or revoked. | No | Check the key. |
| 403 | none | The key lacks `videos:read`. | No | Use the app's open video key. |
| 403 | `app_not_registered` (in `error`) | The key is not bound to an app. | No | Register the app; use that app's key. |
| 403 | `org_suspended` (in `error`) | Your organisation is suspended. | No | Contact Wandura. |
| 404 | `collection_not_found` (in `error`) | Unknown, unpublished or not granted to you. | No | Check the collection code. |
| 429 | `rate_limited` | List limit reached. | After `Retry-After` | Wait; cache the list. |
| 500 | none | The list could not be read. | Backoff | Retry; keep serving your previous list. |
| 503 | `service_unavailable` | Wandura could not check the key in time. | After `Retry-After` | Retry. |

### Get one open video
`GET /api/delivery/v1/videos/{id}`

| Status | Code | When | Retry? | Your backend does |
|---|---|---|---|---|
| 400 | none | `id` is not a UUID. | No | Use the `id` from the list. |
| 400 | `unsupported_version` | Unknown or retired dated version. | No | Send a supported version. |
| 401 | none | Header missing, or key unknown or revoked. | No | Check the key. |
| 403 | none | The key lacks `videos:read`. | No | Use the app's open video key. |
| 403 | `app_not_registered` (in `error`) | The key is not bound to an app. | No | Register the app. |
| 403 | `org_suspended` (in `error`) | Your organisation is suspended. | No | Contact Wandura. |
| 404 | `video_not_found` (in `error`) | Unknown, not granted, unpublished, archived or not playable yet. | No | Drop it from your list. |
| 429 | `rate_limited` | Single-video limit reached (its own counter). | After `Retry-After` | Wait; prefer the cached list. |
| 500 | none | The video could not be read. | Backoff | Retry. |
| 503 | `service_unavailable` | Wandura could not check the key in time. | After `Retry-After` | Retry. |

## Related
- [Canonical API behaviour](https://docs.wandura.ai/developers/reference/behaviour): retry policy, rate limits, freshness.
- [Troubleshooting](https://docs.wandura.ai/developers/production/troubleshooting): from a symptom to a fix.
- [Support](https://docs.wandura.ai/developers/help/support): what to send when an error persists.
