# List licensed catalog

Returns every film and episode your organisation may stream right now. The response is metadata only and never contains a video URL; request playback per episode with [Request playback](https://docs.wandura.ai/developers/reference/playback).

```
GET /api/delivery/v1/catalog
```

**Scope:** `metadata:read`

## Description

A film is included when your organisation has an active streaming licence for it, today (UTC) is inside the licence window, and the film is published. For each included film, all published episodes are returned, plus a placeholder row for each planned episode that is not published yet. Films arrive sorted by `display_order`. There is no pagination. See [Catalog](https://docs.wandura.ai/developers/concepts/catalog).

## Request parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `Authorization` | header | string | Yes | `Bearer <API key>`. |

This endpoint takes no query parameters.

## Request example

```bash
curl -s https://www.wandura.ai/api/delivery/v1/catalog \
  -H "Authorization: Bearer $WANDURA_API_KEY"
```

## Response

`200 OK` with an object containing two arrays.

### Film fields (`films[]`)

| Field | Type | Description |
|---|---|---|
| `id` | string (UUID) | Film ID. It never changes: use it as your primary key, and as `film_ref` in playback. |
| `title` | string | Film title. |
| `slug` | string | URL-safe film identifier, unique across films. Wandura does not change it after the film is created. Also accepted as `film_ref` in playback. |
| `synopsis_short` | string or null | Short description for cards. Length varies. |
| `synopsis_full` | string or null | Full description for detail pages. |
| `genres` | array of strings | Genre labels. Can be empty. |
| `language` | string or null | Language of the film, as free text written by Wandura's content team (for example `en` or `English`). It is not a fixed list of codes: display it, do not parse it. |
| `poster_url` | string or null | Film poster URL. |
| `status` | string | Always `published`. |
| `display_order` | integer | Display position, smallest first. The array is already sorted by it. |
| `planned_episodes` | integer or null | Total number of episodes planned. `null` when not set: then the film gets no placeholder rows, and `episodes` lists only its published episodes. |
| `free_until_episode` | integer or null | Episodes with `episode_no` up to and including this number are free. `0` means no free episode. `null` means no number is set for the film: treat every episode as paid, and ask Wandura if you expected free episodes. |
| `audience` | string or null | `women`, `men`, `lgbtq`, or `null` if unclassified. |
| `form` | string or null | `live_action`, `anime`, or `null` if unclassified. |
| `setting` | string or null | `modern`, `city`, `fantasy`, `campus`, `palace`, or `null` if unclassified. |

### Episode fields (`episodes[]`)

| Field | Type | Description |
|---|---|---|
| `id` | string (UUID) or null | Episode ID. Use it as `episode_id` in playback. `null` on placeholder rows. |
| `film_id` | string (UUID) | ID of the film the episode belongs to. |
| `episode_no` | integer | Episode number, starting at 1. |
| `title` | string or null | Episode title. |
| `synopsis` | string or null | Episode description. Often `null`. |
| `duration_ms` | integer or null | Duration in milliseconds. |
| `poster_uri` | string or null | Episode thumbnail reference. Not guaranteed to be a directly loadable URL. |
| `status` | string | `published`, or `planned` for a placeholder row. |
| `stream_ready` | boolean | `true` when a ticket can be issued for the episode. Always `false` on placeholder rows. |

Within a film, each `episode_no` appears once, so `film_id` + `episode_no` identifies an episode. Episodes are grouped by film ID and sorted by `episode_no` within each film. Take the film order from `films`.

If your organisation has no current licences, both arrays are empty.

## Response example

```json
{
  "films": [
    {
      "id": "3f6c1a52-8d0e-4b7a-9c21-5e4f2a7b9d10",
      "title": "The Heiress Returns",
      "slug": "the-heiress-returns",
      "synopsis_short": "Cast out on her wedding day, a disowned heiress comes back to take everything.",
      "synopsis_full": "Cast out on her wedding day, Mia loses her family, her name and her fortune. Five years later she returns with a new identity and one goal.",
      "genres": ["romance", "revenge"],
      "language": "en",
      "poster_url": "https://cdn.example.com/posters/the-heiress-returns.jpg",
      "status": "published",
      "display_order": 10,
      "planned_episodes": 3,
      "free_until_episode": 1,
      "audience": "women",
      "form": "live_action",
      "setting": "modern"
    }
  ],
  "episodes": [
    {
      "id": "b2a4d6e8-1c3f-4a5b-8d7e-9f0a1b2c3d4e",
      "film_id": "3f6c1a52-8d0e-4b7a-9c21-5e4f2a7b9d10",
      "episode_no": 1,
      "title": "Episode 1",
      "synopsis": null,
      "duration_ms": 94000,
      "poster_uri": null,
      "status": "published",
      "stream_ready": true
    },
    {
      "id": "c7d9e1f3-5a6b-4c8d-9e0f-1a2b3c4d5e6f",
      "film_id": "3f6c1a52-8d0e-4b7a-9c21-5e4f2a7b9d10",
      "episode_no": 2,
      "title": "Episode 2",
      "synopsis": null,
      "duration_ms": 101000,
      "poster_uri": null,
      "status": "published",
      "stream_ready": false
    },
    {
      "id": null,
      "film_id": "3f6c1a52-8d0e-4b7a-9c21-5e4f2a7b9d10",
      "episode_no": 3,
      "title": null,
      "synopsis": null,
      "duration_ms": null,
      "poster_uri": null,
      "status": "planned",
      "stream_ready": false
    }
  ]
}
```

## Errors
| Status | `error_code` | When |
|---|---|---|
| 401 | none | The `Authorization: Bearer` header is missing, or the key is unknown or revoked. |
| 403 | none | The key lacks the `metadata:read` scope. |
| 403 | none (`error` is `org_suspended`) | Your organisation's access is suspended. |
| 429 | `rate_limited` | The rate limit for the catalog and creatives endpoints was reached. |
| 503 | `service_unavailable` | Wandura could not verify the API key in time. The key is not wrong: retry after `Retry-After` seconds, with backoff. |

The two `403` responses carry no `error_code`. Tell them apart by `error`: it is exactly `org_suspended` when your organisation is suspended, and any other text means the key lacks `metadata:read`. This is the one case where you compare `error`; compare the whole string.

See [Errors](https://docs.wandura.ai/developers/concepts/errors#catalog-errors) for what to do in each case.
