WanduraDocsBeta
BetaThese docs are in beta and change often. Something wrong, unclear or missing? Tell us.
Change history

History: Playback and tickets

Every change to this page, newest first. Expand an entry to see what changed.

← Playback and tickets

  1. 3 Oct 2026 · +0 −2 lines
    Show changes
    ⋯
      > **TL;DR** — To play an episode, your backend asks `POST /playback` for a **ticket**: a token for one episode on one device, valid 30 minutes. Send the two device values from the Player SDK, your own stable `viewer_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.
      
    - > **Rolling out.** If Wandura set up a space of your own, you get links from that space instead of tickets: [Your own video space](own-space.md).
    - 
      ## When to request a ticket {#when-to-request}
      
    ⋯
  2. 3 Oct 2026 · +2 −0 lines
    Show changes
    ⋯
      > **TL;DR** — To play an episode, your backend asks `POST /playback` for a **ticket**: a token for one episode on one device, valid 30 minutes. Send the two device values from the Player SDK, your own stable `viewer_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.
      
    + > **Rolling out.** If Wandura set up a space of your own, you get links from that space instead of tickets: [Your own video space](own-space.md).
    + 
      ## When to request a ticket {#when-to-request}
      
    ⋯
  3. 2 Oct 2026 · +41 −68 lines
    Show changes
    - # 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
    + # Playback and tickets
      
    - 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.
    + > **TL;DR** — To play an episode, your backend asks `POST /playback` for a **ticket**: a token for one episode on one device, valid 30 minutes. Send the two device values from the Player SDK, your own stable `viewer_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.
      
    - One request can cover up to three episodes: the one being played plus up to two that follow, with `next` or `episodes`. See [Smooth playback under load](../guides/smooth-playback.md#request-early).
    + ## When to request a ticket {#when-to-request}
      
    - 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.
    + - **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 (`next` or `episodes`). Prefetches are not views.
    + - **Once more** when the Player SDK reports an error, an expired ticket included.
      
    - ## Identify the episode
    + 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](../reference/behaviour.md#tickets).
      
    - You can point at the episode in either of two ways:
    + ## Identify the episode {#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, starting at 1) | `"film_ref": "the-heiress-returns", "episode_no": 3` |
    + | 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` 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`.
    + 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
    + ## Send the device values {#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:
    + 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. 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](../before-you-start.md#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.
    + | `drm_kek` | The device public key generated by the Player SDK. Its private key never leaves the device. |
      
    - Rules for the device values:
    + - Send both or neither. Only one: `400` `invalid_request`.
    + - Send the real SDK output. A value is rejected with `400` `invalid_device_info` when, after trimming, it is shorter than 8 characters or is `(null)`, `null`, `nil`, `undefined`, `nan`, `<null>` or `none` (any case). `fields` names it.
    + - Neither, for an encrypted episode: `409` `sdk_required`.
      
    - - 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`.
    + How to read them in the app: [Player SDK](../player-sdk/index.md).
      
    - ## What the ticket contains
    + ## What the ticket contains {#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](errors.md).
    - 
    - 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.
    + | `play_auth_token` | The token for the Player SDK. A secret: see [Security](security.md#tickets-and-links). |
    + | `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](../guides/subtitles.md). |
      
    - ## Plays, prefetch and `intent`
    + **Check `protection`** on every ticket. Sell paid episodes only when it is `private-encrypted`. Any other value: do not play it, and tell Wandura.
      
    - 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:
    + A batch answers `{ "items": [...] }` in request order, each item with its own `status`. Every field: [Request playback](../reference/playback.md).
      
    - | 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` {#intent}
      
    - `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](rate-limits.md).
    + | 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 |
      
    - Close to your rate limit, prefetch requests are paused first and answered `429` `rate_limited` with `"reason": "prefetch_shed"`, while play requests are still issued. In a batch the prefetch items are refused first, each with its own per-item `429`. A refused prefetch is not an error for the viewer: request the episode with `"intent": "play"` when the viewer taps it. See [Graceful overload](rate-limits.md#graceful-overload).
    + 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](rate-limits.md#graceful-overload).
      
      ## `viewer_ref` {#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.
    - 
    - Send the same `viewer_ref` for the same viewer on the same device. A repeat request for the same episode, with the same `viewer_ref`, `intent` (`play` or `prefetch`) and device values, while the earlier ticket still has at least 5 minutes left, returns that same ticket: same `play_auth_token`, same `expires_at` (never extended). It is not counted toward your rate limit or as a new play. Without `viewer_ref`, every request gets a new ticket.
    + 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](security.md#viewer-data).
      
    - ## Subtitles
    + ## Expiry and recovery {#expiry}
      
    - `subtitles` has one entry per subtitle attached to the episode:
    + A ticket lives 30 minutes (`expires_at`). When the Player SDK reports an error during playback, an expired ticket included:
      
    - | 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`. |
    + 1. Request **one** fresh ticket and reload the player at the current position.
    + 2. If that fails, map its `error_code` to a viewer message ([Errors](errors.md#viewer-messages)).
      
    - - 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.
    + 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
    + ## Related {#related}
      
    - - [Playback reference](../reference/playback.md)
    - - [Errors](errors.md)
    - - [Rate limits](rate-limits.md)
    + - [Playback flow and paywall](../guides/playback-flow.md): the backend endpoint, with code.
    + - [Smooth playback under load](../guides/smooth-playback.md)
    + - [Request playback](../reference/playback.md)
  4. 2 Oct 2026 · +2 −2 lines
    Show changes
    ⋯
      `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](rate-limits.md).
      
    - **Rolling out:** close to your rate limit, prefetch requests are paused first and answered `429` `rate_limited` with `"reason": "prefetch_shed"`, while play requests are still issued. In a batch the prefetch items are refused first, each with its own per-item `429`. A refused prefetch is not an error for the viewer: request the episode with `"intent": "play"` when the viewer taps it. See [Graceful overload](rate-limits.md#graceful-overload).
    + Close to your rate limit, prefetch requests are paused first and answered `429` `rate_limited` with `"reason": "prefetch_shed"`, while play requests are still issued. In a batch the prefetch items are refused first, each with its own per-item `429`. A refused prefetch is not an error for the viewer: request the episode with `"intent": "play"` when the viewer taps it. See [Graceful overload](rate-limits.md#graceful-overload).
      
      ## `viewer_ref` {#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.
      
    - **Rolling out:** send the same `viewer_ref` for the same viewer on the same device. A repeat request for the same episode, with the same `viewer_ref`, `intent` (`play` or `prefetch`) and device values, while the earlier ticket still has at least 5 minutes left, returns that same ticket: same `play_auth_token`, same `expires_at` (never extended). It is not counted toward your rate limit or as a new play. Without `viewer_ref`, every request gets a new ticket.
    + Send the same `viewer_ref` for the same viewer on the same device. A repeat request for the same episode, with the same `viewer_ref`, `intent` (`play` or `prefetch`) and device values, while the earlier ticket still has at least 5 minutes left, returns that same ticket: same `play_auth_token`, same `expires_at` (never extended). It is not counted toward your rate limit or as a new play. Without `viewer_ref`, every request gets a new ticket.
      
      ## Subtitles
    ⋯
  5. 2 Oct 2026 · +5 −1 lines
    Show changes
    ⋯
      `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](rate-limits.md).
      
    - ## `viewer_ref`
    + **Rolling out:** close to your rate limit, prefetch requests are paused first and answered `429` `rate_limited` with `"reason": "prefetch_shed"`, while play requests are still issued. In a batch the prefetch items are refused first, each with its own per-item `429`. A refused prefetch is not an error for the viewer: request the episode with `"intent": "play"` when the viewer taps it. See [Graceful overload](rate-limits.md#graceful-overload).
      
    + ## `viewer_ref` {#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.
      
    + **Rolling out:** send the same `viewer_ref` for the same viewer on the same device. A repeat request for the same episode, with the same `viewer_ref`, `intent` (`play` or `prefetch`) and device values, while the earlier ticket still has at least 5 minutes left, returns that same ticket: same `play_auth_token`, same `expires_at` (never extended). It is not counted toward your rate limit or as a new play. Without `viewer_ref`, every request gets a new ticket.
    + 
      ## Subtitles
      
    ⋯
  6. 2 Oct 2026 · +1 −1 lines
    Show changes
    ⋯
      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.
    + One request can cover up to three episodes: the one being played plus up to two that follow, with `next` or `episodes`. See [Smooth playback under load](../guides/smooth-playback.md#request-early).
      
      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.
    ⋯
  7. 1 Oct 2026 · +6 −6 lines
    Show changes
    ⋯
      ## 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 within minutes.
    + 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.
    ⋯
      | `drm_kek` | The device public key generated by the Player SDK. The matching private key never leaves the device. |
      
    - Your Player SDK onboarding pack names the exact SDK calls. Wandura provides the SDK licence and registers your app's bundle ID or package name during onboarding.
    + The Player SDK guide on partner.wandura.ai → **Developers** → **Player SDK** names the exact SDK calls; it is available after [onboarding](../before-you-start.md#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. Values that look empty, such as the string `"(null)"`, `"null"` or `"undefined"`, or that are shorter than 8 characters, return `400` `invalid_device_info`. The response lists the bad fields in `fields`. This usually means an SDK call returned nothing and your code turned it into a string.
    + - 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`.
      
    ⋯
      ## Plays, prefetch and `intent`
      
    - Each successful ticket request is recorded on Wandura's side and counts as a play. If your app loads episodes before the viewer taps them, tell Wandura with the optional `intent` field:
    + 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 |
    ⋯
      | `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. Prefetch requests still count toward your [rate limit](rate-limits.md).
    + `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](rate-limits.md).
      
      ## `viewer_ref`
    ⋯
      
      - 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.
    + - 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.
      
    ⋯
  8. 1 Oct 2026 · +1 −1 lines
    Show changes
    ⋯
      ### 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 your Wandura partner contact.
    + 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
    ⋯
  9. 29 Sept 2026 · Page created
↑↓ selectEnter openEsc close

Send feedback

Your message goes straight to the Wandura team. It includes the address of this page.

0 / 4000