WanduraDocsBeta
BetaThese docs are in beta and change often. Something wrong, unclear or missing? Tell us.
You are reading an older version of these docs: v1 · Go to the latest (v2)
Change history

History: Request playback

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

← Request playback

  1. 2 Oct 2026 · +4 −1 lines
    Show changes
    ⋯
      ```bash
      curl -s -X POST https://www.wandura.ai/api/delivery/v1/playback \
    -   -H "Authorization: Bearer $WANDURA_PLAYBACK_KEY" \
    +   -H "Authorization: Bearer $WANDURA_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
    ⋯
      | `Wandura-API-Status` | all | API status. `beta` today, on every response, success or error. |
      | `Wandura-Version` | all | The dated API version (YYYY-MM-DD) this response was produced in: the version pinned to your API key, or the one you sent in the `Wandura-Version` request header. Keys created before dated versions existed use `2026-10-02`. |
    + | `Wandura-Request-Id` | all | An opaque id for this request (`req_` followed by 26 characters). Every response carries it, errors included, and error bodies repeat it as `request_id`. Log it, and quote it when you contact Wandura about a request: it finds the exact request in our logs. Do not parse it. |
    + | `Cache-Control` | all | Always `no-store`. Do not cache these responses in shared caches. |
      | `Deprecation` | 200 | Present only when this endpoint, or the dated version serving this response, is deprecated (RFC 9745): `@<unix seconds>` of the deprecation date. Log it in your monitoring. |
      | `Sunset` | 200 | Present only when a deprecated endpoint or dated version has a date when it stops working (RFC 8594), as an HTTP date. |
    ⋯
      | 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. |
    + | 503 | `service_unavailable` | Wandura could not check the licence or the episode in time (`Could not check the licence or episode right now — retry`). This says nothing about your licence or the episode: keep selling, and retry after `Retry-After` seconds, with backoff. |
      
      Example `403`:
    ⋯
  2. 2 Oct 2026 · +4 −7 lines
    Show changes
      # Request playback
      
    - 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.
    + Issues short-lived playback tickets: one episode, or a batch of up to 3 (the episode a viewer starts plus up to two prefetches). Your backend calls this each time a viewer starts an episode and passes the result to the Player SDK on the viewer's device. Lifetime, reuse and batch rules: [Canonical API behaviour](behaviour.md#tickets).
      
      ```
    ⋯
      ```
      
    - **Scope:** `playback` (create it yourself on **Developers** → **Stream films in an app**; if it leaks, anyone can stream your licensed films, so revoke it immediately)
    + **Scope:** `playback` (**Developers** → **Stream films in an app**; see [Authentication](../authentication.md#scopes))
      
      ## Description
    ⋯
      | `Content-Type` | header | string | Recommended | `application/json`. The body must be JSON. |
      | `Wandura-Version` | header | string | No | Optional. A dated API version (YYYY-MM-DD) to use for this request instead of the version pinned to your API key. Unknown or retired values answer 400 with `error_code` `unsupported_version`. |
    + | `Wandura-Simulate` | header | string | No | Optional, test organisations only. Asks Wandura to answer with a simulated error so you can test how your app handles it, for example `rate_limited`, `service_unavailable` or `not_entitled:outside_window`. The simulated response has the same status, body and headers as the real one, plus a `Wandura-Simulated` header naming the scenario. It issues no ticket and does not count against your rate limits, except `partial_batch`, whose first item is a real ticket. Simulated responses have their own limit of 300 per minute per organisation. Keys of production organisations ignore this header completely. An unknown value, or a scenario this endpoint does not support, is ignored and the request runs normally. See the Testing guide for the list of scenarios. |
      | `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`. |
    ⋯
      | `next` | body | integer | No | Batch form B — with an anchor episode (`episode_id`, or `film_ref` + `episode_no`), also issue tickets for the next N episodes of the same film. Following episodes are recorded as `prefetch`. An episode past the end of the film, or not published yet, is a per-item 404 `not_ready`. Sending `next` (even 0) switches the response to the batch shape. |
      
    - `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
      
    ⋯
      | `items[].retry_after` | integer | Seconds to wait before retrying this item (503 and 429). |
      
    - Subtitles are returned in `lang` order.
    - 
      ## Response example
      
    ⋯
      ```
      
    - See [Errors](../concepts/errors.md#playback-errors) for what to do in each case.
    + What to do in each case: [Errors](../concepts/errors.md#playback-errors).
  3. 2 Oct 2026 · +4 −4 lines
    Show changes
    ⋯
      | `drm_kek` | body | string | Yes, for encrypted episodes | Device public key generated by the Player SDK. Send together with `union_info`. Same validation as `union_info`. The matching private key never leaves the device. |
      | `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 (up to 256 characters). Do not include personal data. |
    + | `viewer_ref` | body | string | No | Your own opaque session or device identifier, stored with the ticket for support lookups (up to 256 characters). Do not include personal data. When you send it, asking again for the same episode with the same `viewer_ref`, intent (`play` or `prefetch`) and device values while your previous ticket still has at least 5 minutes left returns that same ticket (same `expires_at`, not extended) and does not count against your rate limit. |
      | `episodes` | body | array | No | Batch form A — up to 3 episodes in one call (typically the one being watched + the next 2). Cannot be combined with top-level `episode_id` / `film_ref` / `episode_no` or with `next`. Item 0 takes the top-level `intent`; later items default to `prefetch` unless they carry their own. |
      | `episodes[].episode_id` | body | string (UUID) | No | Episode ID. Or send `film_ref` + `episode_no`. |
    ⋯
      | `subtitles[].url` | string | URL of the subtitle file. Not tied to the ticket's expiry. |
      | `subtitles[].format` | string | Always `webvtt`. |
    - | `items` | array | Batch only — one entry per requested episode, in request order. On `status` 200 the entry also has `episode_id`, `episode_no` and the same ticket fields as a single response; otherwise `error`, `error_code`, optional `reason` (403) and `retry_after` (503), and no episode data. |
    + | `items` | array | Batch only — one entry per requested episode, in request order. On `status` 200 the entry also has `episode_id`, `episode_no` and the same ticket fields as a single response; otherwise `error`, `error_code`, optional `reason` (403, and 429 `prefetch_shed`) and `retry_after` (503 and 429), and no episode data. |
      | `items[].request` | object | Echo of what was asked for this item. |
      | `items[].status` | integer | This item's own HTTP-equivalent status. |
    ⋯
      | `items[].subtitles[].url` | string | URL of the subtitle file. Not tied to the ticket's expiry. |
      | `items[].subtitles[].format` | string | Always `webvtt`. |
    - | `items[].retry_after` | integer | Seconds to wait before retrying this item (503 only). |
    + | `items[].retry_after` | integer | Seconds to wait before retrying this item (503 and 429). |
      
      Subtitles are returned in `lang` order.
    ⋯
      | 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`. |
    + | 429 | `rate_limited` | The playback rate limit was reached, or (with `reason` `prefetch_shed`) prefetch is paused because your playback traffic is close to it. 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. |
    ⋯
  4. 2 Oct 2026 · +6 −0 lines
    Show changes
    ⋯
      | `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. |
    + | `Wandura-Version` | header | string | No | Optional. A dated API version (YYYY-MM-DD) to use for this request instead of the version pinned to your API key. Unknown or retired values answer 400 with `error_code` `unsupported_version`. |
      | `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`. |
    ⋯
      |---|---|---|
      | `Wandura-API-Status` | all | API status. `beta` today, on every response, success or error. |
    + | `Wandura-Version` | all | The dated API version (YYYY-MM-DD) this response was produced in: the version pinned to your API key, or the one you sent in the `Wandura-Version` request header. Keys created before dated versions existed use `2026-10-02`. |
    + | `Deprecation` | 200 | Present only when this endpoint, or the dated version serving this response, is deprecated (RFC 9745): `@<unix seconds>` of the deprecation date. Log it in your monitoring. |
    + | `Sunset` | 200 | Present only when a deprecated endpoint or dated version has a date when it stops working (RFC 8594), as an HTTP date. |
    + | `Link` | 200 | Present with `Deprecation`: `<https://docs.wandura.ai/…>; rel="deprecation"` (and `rel="sunset"`) pointing to the page that explains what to use instead. |
      | `Retry-After` | 429, 503 | Seconds to wait before retrying. Sent with 429 and with retryable 503 responses. |
      | `X-RateLimit-Limit` | 429 | Requests (for playback, tickets) allowed per window for this endpoint group and your organisation. |
    ⋯
      | 400 | `invalid_request` | Only one of `union_info` and `drm_kek` was sent, or a malformed batch (`episodes` empty or longer than 3, an item without an episode, `episodes` mixed with top-level episode fields or with `next`, `next` outside 0–2 or without an anchor). |
      | 400 | `invalid_device_info` | A device value looks empty or is too short. The `fields` array names it. |
    + | 400 | `unsupported_version` | The `Wandura-Version` header, or the version pinned to your key, is unknown or retired. The body adds `supported_versions` and `latest_version` (and `upgrade_to` when your pin was retired). |
      | 401 | none | The `Authorization: Bearer` header is missing, or the key is unknown or revoked. |
      | 403 | none | The key lacks the `playback` scope. |
    ⋯
  5. 2 Oct 2026 · +30 −7 lines
    Show changes
    ⋯
      | `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`. |
    + | `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`. 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` (any letter case). |
    + | `drm_kek` | body | string | Yes, for encrypted episodes | Device public key generated by the Player SDK. Send together with `union_info`. Same validation as `union_info`. The matching private key never leaves the device. |
      | `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. |
    + | `viewer_ref` | body | string | No | Your own opaque session or device identifier, stored with the ticket for support lookups (up to 256 characters). Do not include personal data. |
    + | `episodes` | body | array | No | Batch form A — up to 3 episodes in one call (typically the one being watched + the next 2). Cannot be combined with top-level `episode_id` / `film_ref` / `episode_no` or with `next`. Item 0 takes the top-level `intent`; later items default to `prefetch` unless they carry their own. |
    + | `episodes[].episode_id` | body | string (UUID) | No | Episode ID. Or send `film_ref` + `episode_no`. |
    + | `episodes[].film_ref` | body | string | No | Film `id` or `slug`, with `episode_no`. |
    + | `episodes[].episode_no` | body | integer | No | Episode number (whole number from 1), with `film_ref`. |
    + | `episodes[].intent` | body | string | No | Intent of this item. |
    + | `next` | body | integer | No | Batch form B — with an anchor episode (`episode_id`, or `film_ref` + `episode_no`), also issue tickets for the next N episodes of the same film. Following episodes are recorded as `prefetch`. An episode past the end of the film, or not published yet, is a per-item 404 `not_ready`. Sending `next` (even 0) switches the response to the batch shape. |
      
      `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).
    ⋯
      | Field | Type | Description |
      |---|---|---|
    - | `vid` | string | Video ID to load in the Player SDK. |
    + | `vid` | string | Video ID to load in the Player SDK. Present with `play_auth_token`. |
      | `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. |
    + | `manifest_url` | string | Signed stream link, present instead of `vid` + `play_auth_token` only for content Wandura delivers to your organisation without encryption, by agreement (then `protection` is `signed-url-only`). Valid until `expires_at`. |
    + | `expires_at` | string (ISO 8601, UTC) | When the ticket expires: 30 minutes after it was issued (15 minutes for `manifest_url`). |
      | `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` | array | Subtitles attached to the episode, in `lang` order. 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`. |
    + | `items` | array | Batch only — one entry per requested episode, in request order. On `status` 200 the entry also has `episode_id`, `episode_no` and the same ticket fields as a single response; otherwise `error`, `error_code`, optional `reason` (403) and `retry_after` (503), and no episode data. |
    + | `items[].request` | object | Echo of what was asked for this item. |
    + | `items[].status` | integer | This item's own HTTP-equivalent status. |
    + | `items[].subtitles[].lang` | string | Language code, for example `en`, `vi` or `pt-BR`. |
    + | `items[].subtitles[].url` | string | URL of the subtitle file. Not tied to the ticket's expiry. |
    + | `items[].subtitles[].format` | string | Always `webvtt`. |
    + | `items[].retry_after` | integer | Seconds to wait before retrying this item (503 only). |
      
      Subtitles are returned in `lang` order.
    ⋯
      ```
      
    + ## Response headers {#response-headers}
    + 
    + | Header | Responses | Description |
    + |---|---|---|
    + | `Wandura-API-Status` | all | API status. `beta` today, on every response, success or error. |
    + | `Retry-After` | 429, 503 | Seconds to wait before retrying. Sent with 429 and with retryable 503 responses. |
    + | `X-RateLimit-Limit` | 429 | Requests (for playback, tickets) allowed per window for this endpoint group and your organisation. |
    + | `X-RateLimit-Window` | 429 | Window length in seconds. |
    + 
      ## Errors
      
    ⋯
      |---|---|---|
      | 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_request` | Only one of `union_info` and `drm_kek` was sent, or a malformed batch (`episodes` empty or longer than 3, an item without an episode, `episodes` mixed with top-level episode fields or with `next`, `next` outside 0–2 or without an anchor). |
      | 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. |
    ⋯
  6. 1 Oct 2026 · +2 −1 lines
    Show changes
    ⋯
      ```
      
    - **Scope:** `playback` (issued by Wandura only)
    + **Scope:** `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
    ⋯
      | 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`:
    ⋯
  7. 1 Oct 2026 · +3 −2 lines
    Show changes
    ⋯
      | `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. Other values are recorded as unknown and never rejected. |
    + | `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 they look empty (for example `"(null)"`, `"null"`, `"nil"`, `"undefined"`, `"none"`) or are shorter than 8 characters.
    + `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
    ⋯
      | 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. |
      
      Example `403`:
    ⋯
  8. 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