# Catalog

> **TL;DR** — `GET /catalog` lists every film and episode your organisation may stream right now, with display order, free-episode rule and a readiness flag per episode. Metadata only, never a video URL. Fetch it whole, or in pages with `limit` and `cursor`. It is the only film list your app needs; a ticket request re-checks everything anyway.

## What is in the catalog
A film is listed when, at the moment of the request:

- your organisation holds a streaming licence for it, and the licence is active;
- today (UTC) is inside the licence window — whole days, first and last day included;
- the film is published.

For those films it lists every published episode, plus a placeholder row for each planned episode not yet published. Films still in production and review cuts never appear and cannot be played through the API. An empty response (`"films": []`) means no licence is active for your organisation right now.

## Response shape
Two flat arrays, `films` and `episodes`; join them on `episodes[].film_id` = `films[].id`. The response also carries `links` (`self`, `next`, `playback`, `config`). Every field: [List licensed catalog](https://docs.wandura.ai/v1/developers/reference/catalog).

## Pages
Without `limit` or `cursor` you receive the whole catalog in one response. With `limit` (1–200 films) you receive one page of films with **all** their episodes, plus `next_cursor` and `links.next`; pass the cursor back unchanged until it is `null`. Pages are **not** one consistent snapshot: the rules for syncing safely are in [Canonical API behaviour · Pagination](https://docs.wandura.ai/v1/developers/reference/behaviour#pagination), and the code in [Catalog sync](https://docs.wandura.ai/v1/developers/guides/catalog-sync).

## Order
`films` arrives sorted by `display_order`, smallest first, and across pages too. Render films in that order; Wandura edits it, so keep no ordering table of your own. `episodes` arrives grouped by film, then by `episode_no`; take the film order from `films`.

## Free and paid episodes
Episodes with `episode_no` ≤ the film's `free_until_episode` are free; the rest are paid. `0` means no free episode; `null` means none is set, so treat every episode as paid. The value differs per film and can change: read it on every sync.

## Planned episodes
`planned_episodes` is the number of episodes the film will have. For each one not yet published, the catalog includes a placeholder row so you can show "coming soon":

| Field | Value |
|---|---|
| `id` | `null` |
| `status` | `"planned"` |
| `stream_ready` | `false` |
| `title`, `synopsis`, `duration_ms`, `poster_uri`, `poster_url` | `null` |

Never send a placeholder to `POST /playback`: it has no ID. When `planned_episodes` is `null`, there are no placeholders.

## Readiness: `stream_ready`
`true` means a ticket can be issued for the episode now. Grey out or hide episodes where it is `false`. It is a snapshot of the moment you fetched the catalog: the ticket request checks again, and its answer is the one that counts. A `not_ready` answer for an episode that was `stream_ready: true` is normal, for example right after a withdrawal.

## Genre facets
| Field | Values (`null` = not classified; new values may be added) |
|---|---|
| `audience` | `women`, `men`, `lgbtq` |
| `form` | `live_action`, `anime` |
| `setting` | `modern`, `city`, `fantasy`, `campus`, `palace` |

Films also carry `genres` (free-text labels) and `language` (free text: display it, do not parse it).

## Text and artwork
- `synopsis_short` for cards, `synopsis_full` for detail pages; lengths vary, clamp them. Either can be `null`.
- `poster_url` (film) can be `null` while artwork is prepared: show a placeholder.
- For episodes use `poster_url` (a loadable `https` URL or `null`, then fall back to the film poster). `poster_uri` is an internal reference, not a loadable URL.

Each film also carries `images`, its artwork by kind, each entry `{ "kind", "url", "width", "height" }`:

| `kind` | What it is | `width` / `height` |
|---|---|---|
| `poster_3_4` | 3:4 card poster | pixels, measured from the file |
| `cover_9_16` | 9:16 full-screen cover, the same image as `poster_url` | `null` for now (size not recorded yet) |

A kind Wandura does not have is left out, so the array can be empty; it never holds a `null` entry. Every `url` is an absolute `https` URL on a Wandura host that you can load directly. New kinds may be added: skip a kind you do not know. `poster_url` stays as it is.

## Freshness
A change on Wandura's side (a publication, a withdrawal, a licence that starts or ends) appears in the catalog within 30 seconds. All bounds: [Canonical API behaviour · Freshness](https://docs.wandura.ai/v1/developers/reference/behaviour#freshness). Sync every 15–60 minutes from your server; never call the catalog on app launch.

## Related
- [Catalog sync](https://docs.wandura.ai/v1/developers/guides/catalog-sync)
- [List licensed catalog](https://docs.wandura.ai/v1/developers/reference/catalog)
- [When rights end](https://docs.wandura.ai/v1/developers/production/rights-end)
