Issues a short-lived playback ticket for one episode. Your backend calls this each time a viewer starts an episode and passes the result to the Player SDK on the viewer's device.
POST /api/delivery/v1/playbackScope: playback (create it yourself on Developers → Stream films in an app; if it leaks, anyone can stream your licensed films, so revoke it immediately)
Description
Wandura checks, on every request, that your organisation is active and that your streaming licence for the episode's film is active and inside its window. It then returns a play token bound to the viewer's device. Licensed episodes are encrypted and play only through the Player SDK, so you must send the device values union_info and drm_kek. See Playback.
Call it only from your backend. The endpoint sends no CORS headers.
Request parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
Authorization |
header | string | Yes | Bearer <API key> for a key with the playback scope. |
Content-Type |
header | string | Recommended | application/json. The body must be JSON. |
episode_id |
body | string (UUID) | One of episode_id or film_ref + episode_no |
The episode's id from the catalog. Takes precedence if you also send film_ref. |
film_ref |
body | string | With episode_no, if no episode_id |
The film's id or slug. |
episode_no |
body | integer | With film_ref, if no episode_id |
Episode number, starting at 1. Must be a JSON number, not a string. |
union_info |
body | string | Yes, for encrypted episodes | Device ID generated by the Player SDK on the viewer's device. Send together with drm_kek. |
drm_kek |
body | string | Yes, for encrypted episodes | Device public key generated by the Player SDK. Send together with union_info. |
intent |
body | string | No | play when the viewer tapped the episode, prefetch when your app loads it ahead of a tap. Prefetch requests are not counted as plays. A missing or other value is recorded as unknown, never rejected, and counted as a play. |
viewer_ref |
body | string | No | Your own opaque session or device identifier, stored with the ticket for support lookups. Do not include personal data. |
union_info and drm_kek are rejected with 400 invalid_device_info when, after trimming spaces, a value is shorter than 8 characters or is one of (null), null, nil, undefined, nan, <null> or none (in any letter case).
Request example
curl -s -X POST https://www.wandura.ai/api/delivery/v1/playback \
-H "Authorization: Bearer $WANDURA_PLAYBACK_KEY" \
-H "Content-Type: application/json" \
-d '{
"film_ref": "the-heiress-returns",
"episode_no": 1,
"union_info": "<device ID from the Player SDK>",
"drm_kek": "<device public key from the Player SDK>",
"intent": "play",
"viewer_ref": "u_48213:dev_9a7c"
}'Response
200 OK
| Field | Type | Description |
|---|---|---|
vid |
string | Video ID to load in the Player SDK. |
play_auth_token |
string | Play token to hand to the Player SDK. Keep it secret and do not log it. |
expires_at |
string (ISO 8601, UTC) | When the ticket expires: 30 minutes after it was issued. |
protection |
string | Protection level of what you received. private-encrypted for licensed episodes. Only sell paid episodes when it is private-encrypted. |
subtitles |
array | Subtitles attached to the episode. Empty when none are attached. |
subtitles[].lang |
string | Language code, for example en, vi or pt-BR. |
subtitles[].url |
string | URL of the subtitle file. Not tied to the ticket's expiry. |
subtitles[].format |
string | Always webvtt. |
Subtitles are returned in lang order.
Response example
{
"vid": "v0d2f8c1a9e4b7",
"play_auth_token": "eyJhbGciOi...placeholder...",
"expires_at": "2026-10-01T12:30:00.000Z",
"protection": "private-encrypted",
"subtitles": [
{ "lang": "en", "url": "https://cdn.example.com/subtitles/heiress-ep1-en.vtt", "format": "webvtt" },
{ "lang": "vi", "url": "https://cdn.example.com/subtitles/heiress-ep1-vi.vtt", "format": "webvtt" }
]
}Errors
| Status | error_code |
When |
|---|---|---|
| 400 | none | The body is not valid JSON, or has neither episode_id nor film_ref with a numeric episode_no. |
| 400 | invalid_request |
Only one of union_info and drm_kek was sent. |
| 400 | invalid_device_info |
A device value looks empty or is too short. The fields array names it. |
| 401 | none | The Authorization: Bearer header is missing, or the key is unknown or revoked. |
| 403 | none | The key lacks the playback scope. |
| 403 | not_entitled |
Your organisation may not stream this film right now. reason is one of org_suspended, entitlement_not_found, entitlement_suspended, streaming_disabled, outside_window. |
| 404 | not_ready |
The film or episode is unknown, not published, or not ready to stream. |
| 409 | sdk_required |
The episode is encrypted and the request had no device values. The body also carries protection. |
| 429 | rate_limited |
The playback rate limit was reached. See Retry-After. |
| 502 | none | The stream could not be prepared or the play token could not be signed. Retry with backoff. |
| 503 | not_ready |
No playable version of the episode is available right now. |
| 503 | service_unavailable |
Wandura could not verify the API key in time. The key is not wrong: retry after Retry-After seconds, with backoff. |
| 503 | service_unavailable |
The video provider did not answer in time (The video provider did not answer in time — retry). The episode is not missing: retry after Retry-After seconds, with backoff. |
Example 403:
{
"error": "outside_window",
"error_code": "not_entitled",
"reason": "outside_window"
}See Errors for what to do in each case.
Updated 1 Oct 2026 · Change history (3)