# Errors

The Delivery API uses standard HTTP status codes and adds a machine-readable code to most errors. The catalog and playback endpoints return a flat error object with an `error_code` string; the creatives endpoints return an envelope with a numeric `code`. This page lists every error each endpoint returns today, what it means and what to do.

## Error formats

### Catalog and playback

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

| Field | Type | Description |
|---|---|---|
| `error` | string | Human-readable message. Log it, but do not parse it, except for the catalog `403` below. |
| `error_code` | string | Machine-readable code. Branch on this. Absent on some errors, listed below as "none". |
| `reason` | string | Only with `not_entitled`: the detailed reason, for logs and support. |
| `fields` | array of strings | Only with `invalid_device_info`: which device values were rejected. |
| `protection` | string | Only with `sdk_required`: the protection level of the episode. |

### Creatives

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

| Field | Type | Description |
|---|---|---|
| `code` | integer | `0` on success, a four-digit error code otherwise. The first three digits are the HTTP status. |
| `message` | string | Human-readable message. Do not parse it. |
| `server_time` | integer | Server time in Unix epoch seconds. |

The one exception is `429`: the creatives endpoints return it in the flat format, with `error_code` `rate_limited`. See [Rate limits](https://docs.wandura.ai/developers/concepts/rate-limits).

## Handle errors by code

- Branch on `error_code` (or `code` for creatives), not on the HTTP status alone. Several different situations share a status.
- Always keep a default branch. Wandura may add new codes within `v1`, and an app without a default branch shows a blank screen when one appears.
- Never show `error` or `message` text to viewers. Map codes to your own copy.

## Catalog errors

`GET /api/delivery/v1/catalog`

| Status | `error_code` | Message | Meaning | What to do |
|---|---|---|---|---|
| 401 | none | `Missing Bearer API key` | No `Authorization: Bearer` header. | Fix the request. Do not retry. |
| 401 | none | `Invalid or revoked API key` | The key is unknown or revoked. | Check the key. Do not retry. |
| 403 | none | `This API key does not have the metadata:read scope` | The key lacks the scope. | Use a key with `metadata:read`. |
| 403 | none | `org_suspended` | Your organisation's access is suspended. | Contact Wandura. Do not retry. |
| 429 | `rate_limited` |  | Rate limit reached. | Wait for `Retry-After`. |
| 503 | `service_unavailable` | `Could not verify the API key right now — retry` | Wandura could not check the key in time. The key is **not** wrong. | Retry after `Retry-After` seconds, with backoff. Do not rotate the key. |

## Playback errors

`POST /api/delivery/v1/playback`

| Status | `error_code` | Meaning | What to do |
|---|---|---|---|
| 400 | none | The body is not valid JSON, or has neither `episode_id` nor `film_ref` with a numeric `episode_no`. | Fix the request. |
| 400 | `invalid_request` | Only one of `union_info` and `drm_kek` was sent. | Send both. |
| 400 | `invalid_device_info` | A device value looks empty or invalid, for example the string `"(null)"`. `fields` names the bad values. | Fix how your app reads the values from the Player SDK. |
| 401 | none | Missing, invalid or revoked API key. | Check the key. Do not retry. |
| 403 | none | The key lacks the `playback` scope. | Use a playback key: create one on **Developers** → **Stream films in an app**. |
| 403 | `not_entitled` | Your organisation is not licensed to stream this film right now. `reason` gives the detail. | Show your locked or unavailable state. Do not retry. |
| 404 | `not_ready` | The episode or film is unknown, not published, or not ready to stream. | Show "not available yet". Refresh your catalog. |
| 409 | `sdk_required` | The episode is encrypted and the request had no device values. | Resend with `union_info` and `drm_kek`. |
| 429 | `rate_limited` | Rate limit reached. | Wait for `Retry-After`. |
| 502 | none | Wandura could not prepare the stream or sign the play token. | Retry with backoff. |
| 503 | `not_ready` | No playable version of this episode is available right now. | Show "not available yet". Retry later. |
| 503 | `service_unavailable` | Wandura could not check the API key in time. The key is **not** wrong. | Retry after `Retry-After` seconds, with backoff. Do not rotate the key. |
| 503 | `service_unavailable` | The video provider did not answer in time (playback only). Nothing is wrong with the episode or your key. | Retry after `Retry-After` seconds (2), with backoff. |

### `not_entitled` reasons

With `not_entitled`, the `reason` field says why. Log it and quote it to support, but do not build different screens for each reason: they all mean "not licensed right now", and 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 is outside your licence window for this film. |

## Creatives errors

`GET /api/delivery/v1/creatives` and `GET /api/delivery/v1/creatives/{id}`

| Status | `code` | Message | Meaning | What to do |
|---|---|---|---|---|
| 401 | 4010 | `Missing Bearer API key` | No `Authorization: Bearer` header. | Fix the request. |
| 401 | 4011 | `Invalid or revoked API key` | The key is unknown or revoked. | Check the key. Do not retry. |
| 403 | 4030 | `This API key does not have the creatives:export scope` | The key lacks the scope. | Use a key with `creatives:export`. |
| 403 | 4031 | `org_suspended` | Your organisation's access is suspended. | Contact Wandura. |
| 404 | 4040 | `Creative not found` | `GET /creatives/{id}` only. The creative does not exist, belongs to another organisation or is not approved. | Remove it from your active set. |
| 422 | 4220 | `limit must be an integer between 1 and 100` | List only. Bad `limit`. | Fix the request. |
| 422 | 4221 | `updated_since must be epoch seconds` | List only. Bad `updated_since`. | Fix the request. |
| 422 | 4222 | `cursor is not valid` | List only. The cursor was changed or truncated. | Restart from your last checkpoint. |
| 429 | none (flat body, `error_code` `rate_limited`) |  | Rate limit reached. | Wait for `Retry-After`. |
| 503 | 5030 | `The creative feed is temporarily unavailable` | Temporary problem on Wandura's side. | Retry with backoff. |
| 503 | 5031 | `Could not verify the API key right now — retry` | Wandura could not check the key in time. The key is **not** wrong. | Retry with backoff. Do not rotate the key. |

## Retry guidance

| Situation | Retry? |
|---|---|
| `400`, `401`, `403`, `409`, `422` | No. Fix the request, the key or the licence first. |
| `404` / `not_ready` | Not immediately. Refresh your catalog and try again later. |
| `429` | Yes, after the number of seconds in `Retry-After`. |
| `502`, `503`, other `5xx` | Yes, with exponential backoff and a cap, for example 1 s, 2 s, 4 s, then give up. |

During playback, request a fresh ticket once when the Player SDK fails, then follow this table. See [Playback](https://docs.wandura.ai/developers/concepts/playback#expiry).

## Getting help

If an error persists, contact support with the details listed in [Support](https://docs.wandura.ai/developers/help/support).
