On this page
TL;DR — To play an episode, your backend asks
POST /playbackfor a ticket: a token for one episode on one device, valid 30 minutes. Send the two device values from the Player SDK, your own stableviewer_ref, and"intent": "play"when a viewer starts an episode. One call can return up to 3 tickets (the episode and the next two). Reusing a ticket for the same viewer, device and episode is fine until 5 minutes before it expires.
When to request a ticket
- When a viewer starts an episode: always a request with
"intent": "play"for it. That request is what counts the view. - Ahead of time for the next one or two episodes, with
"intent": "prefetch", in the same request (nextorepisodes). Prefetches are not views. - Once more when the Player SDK reports an error, an expired ticket included.
Never request tickets for a whole film, and never for an episode your paywall has not unlocked: Wandura checks your licence, not your viewers. The lifetime, reuse and batch rules are in one place: Canonical API behaviour · Playback tickets.
Identify the episode
| 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 from 1) |
"film_ref": "the-heiress-returns", "episode_no": 3 |
If you send both, episode_id wins. Use the same form each time for the same episode: ticket reuse matches the episode as you sent it.
Send the device values
Licensed episodes are encrypted, and only the Player SDK can decrypt them. Before each request, your app reads two values from the SDK and sends 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. Its private key never leaves the device. |
- Send both or neither. Only one:
400invalid_request. - Send the real SDK output. A value is rejected with
400invalid_device_infowhen, after trimming, it is shorter than 8 characters or is(null),null,nil,undefined,nan,<null>ornone(any case).fieldsnames it. - Neither, for an encrypted episode:
409sdk_required.
How to read them in the app: Player SDK.
What the ticket contains
| Field | Description |
|---|---|
vid |
The video ID to load in the Player SDK. |
play_auth_token |
The token for the Player SDK. A secret: see Security. |
expires_at |
When it expires (ISO 8601, UTC). |
protection |
private-encrypted for licensed episodes. |
subtitles |
WebVTT files, one per language; can be empty. See Subtitles. |
Check protection on every ticket. Sell paid episodes only when it is private-encrypted. Any other value: do not play it, and tell Wandura.
A batch answers { "items": [...] } in request order, each item with its own status. Every field: Request playback.
intent
| Value | Meaning | Counted as a view |
|---|---|---|
play |
The viewer just started the episode. | Yes |
prefetch |
Your app loads it ahead of time. | No |
| missing or anything else | Recorded as unknown, never rejected. | Yes |
Near your rate limit, prefetch tickets are refused first (429 with "reason": "prefetch_shed") so that play tickets keep flowing. A refused prefetch is not an error for the viewer: request the episode again with "intent": "play" when the viewer gets there. See Rate limits.
viewer_ref
An optional opaque string of yours, at most 256 characters, stored with the ticket for support lookups. Make it stable per viewer and device and free of personal data, for example an HMAC of your user ID plus a hash of your install ID. With it, Wandura can return the same ticket to a repeat request instead of issuing (and counting) a new one. Privacy and retention: Security.
Expiry and recovery
A ticket lives 30 minutes (expires_at). When the Player SDK reports an error during playback, an expired ticket included:
- Request one fresh ticket and reload the player at the current position.
- If that fails, map its
error_codeto a viewer message (Errors).
Do not branch on the Player SDK's own error codes (they change between versions) and never retry in a loop. When the app comes back from the background with less than a minute left, replace the ticket before resuming.
Related
- Playback flow and paywall: the backend endpoint, with code.
- Smooth playback under load
- Request playback
Updated 3 Oct 2026 · Change history (9)