# Playback

To play an episode, your backend asks Wandura for a playback ticket with `POST /api/delivery/v1/playback`, and the viewer's Player SDK uses that ticket to load the encrypted stream. This page explains how to request a ticket, what it contains, how long it lasts and what to do when it expires.

## When to request a ticket

Request one ticket per play start: when the viewer taps an episode, or when your app loads the next episode ahead of time. Do not request tickets in bulk and store them, because each one expires 30 minutes after it is issued.

Each request covers exactly one episode. There is no batch endpoint.

Wandura checks your licence on every request: your organisation must be active, and your streaming licence for the film must be active and inside its window. A catalog entry does not guarantee a ticket; the ticket request is the final check.

## Identify the episode

You can point at the episode in either of two ways:

| Option | Fields | Example |
|---|---|---|
| By episode ID | `episode_id` | `"episode_id": "b2a4d6e8-1c3f-4a5b-8d7e-9f0a1b2c3d4e"` |
| By film and number | `film_ref` (film ID or slug) and `episode_no` (a JSON number, starting at 1) | `"film_ref": "the-heiress-returns", "episode_no": 3` |

If you send both, `episode_id` is used. An unknown film, an unknown episode and an episode that is not ready all return the same `404` with `error_code` `not_ready`.

## Send the device values

Licensed episodes are encrypted, and only the Player SDK can decrypt them. A standard HLS player loads the playlist and then stalls, without an error that explains why. Before each ticket request, the app on the viewer's device asks the Player SDK for two values and passes them to your backend:

| Field | What it is |
|---|---|
| `union_info` | The device ID generated by the Player SDK. |
| `drm_kek` | The device public key generated by the Player SDK. The matching private key never leaves the device. |

The Player SDK guide on partner.wandura.ai → **Developers** → **Player SDK** names the exact SDK calls; it is available after [onboarding](https://docs.wandura.ai/developers/before-you-start#get-onboarded) to members with the **Developer** or **Admin** role. Wandura registers your app IDs and issues the Player SDK licence; your organisation pays for it.

Rules for the device values:

- Send both or neither. Sending only one returns `400` `invalid_request`.
- Send the real SDK output. A value is rejected with `400` `invalid_device_info` when, after trimming spaces, it is shorter than 8 characters or is one of `(null)`, `null`, `nil`, `undefined`, `nan`, `<null>` or `none` (in any letter case). The response lists the bad fields in `fields`. This usually means an SDK call returned nothing and your code turned it into a string.
- Sending neither for an encrypted episode returns `409` `sdk_required`.

## What the ticket contains

| Field | Description |
|---|---|
| `vid` | The video ID to load in the Player SDK. |
| `play_auth_token` | The play token to hand to the Player SDK. Treat it as a secret. |
| `expires_at` | When the ticket expires, as an ISO 8601 UTC timestamp. |
| `protection` | The protection level of what you received. `private-encrypted` for licensed episodes. |
| `subtitles` | Every subtitle attached to the episode. Can be an empty array. |

On the device, set the video ID and the play token on the Player SDK and start playback. The SDK fetches the playlist and the keys referenced in it by itself; your code does not handle keys.

### Check `protection`

Read `protection` on every response. Only offer paid episodes when it is `private-encrypted`. If you ever receive a different value, do not sell the episode and contact Wandura.

## Expiry

A ticket is valid for 30 minutes from the moment it is issued. `expires_at` gives the exact deadline.

When the Player SDK reports an error during playback, including an expired ticket:

1. Request one fresh ticket from your backend and reload the player at the current position.
2. If that request fails, read `error_code` from the API response to decide what to tell the viewer. See [Errors](https://docs.wandura.ai/developers/concepts/errors).

Do not branch on the Player SDK's own error codes to detect expiry; they vary between SDK versions. Retry once, not in a loop.

## Plays, prefetch and `intent`

Each successful ticket request is recorded on Wandura's side and counts as a play, unless it is marked as a prefetch. If your app loads episodes before the viewer taps them, tell Wandura with the optional `intent` field:

| Value | Meaning |
|---|---|
| `play` | The viewer just tapped the episode. |
| `prefetch` | Your app is loading the episode ahead of a tap. Not counted as a play. |

`intent` never changes the response. A missing or misspelled value is recorded as unknown, not rejected, and the request counts as a play: only `prefetch` is left out of the play count. Prefetch requests still count toward your [rate limit](https://docs.wandura.ai/developers/concepts/rate-limits).

## `viewer_ref`

`viewer_ref` is an optional, opaque string that Wandura stores with the ticket for support lookups. Use your own session or device identifier, such as `<user-id>:<device-id>`. Do not put personal data, such as email addresses or names, in it.

## Subtitles

`subtitles` has one entry per subtitle attached to the episode:

| Field | Description |
|---|---|
| `lang` | Language code as delivered by the studio, for example `en`, `vi` or `pt-BR`. |
| `url` | Where to download the subtitle file. |
| `format` | Always `webvtt`. |

- An empty array means no subtitles are attached yet. It is not an error.
- Subtitle URLs are not tied to the ticket's expiry, so you can keep using them after the ticket expires. When Wandura replaces a subtitle file, it gets a new URL and the old one stops working: if a stored URL fails, take the current one from a fresh ticket.
- Native players can take the URL directly. In a web player, do not add the `crossorigin` attribute to the video element to load subtitles as a `<track>`: that changes how the video itself is requested and can stop it from playing. Download the WebVTT file with `fetch` and add the cues with `video.addTextTrack()` instead.

## Related

- [Playback reference](https://docs.wandura.ai/developers/reference/playback)
- [Errors](https://docs.wandura.ai/developers/concepts/errors)
- [Rate limits](https://docs.wandura.ai/developers/concepts/rate-limits)
