Playback API

Authorize one viewer to watch one episode and receive a hosted webview link for immediate WebView playback plus equivalent signed HLS/DASH manifests for your own player. Optional non-PII preferences personalize the webview. Requires playback:token.

Endpoints

MethodPathPurpose
POST/playback/tokensAuthorize an episode → webview link plus signed manifest
GET/playback/sessions/{session_id}Get a playback session
POST/playback/sessions/{session_id}/revokeEnd an active session

Token request

curl -X POST "https://signal-partners.newunivers.ai/v1/playback/tokens" \
  -H "Authorization: Bearer nsp_live_xxx" \
  -H "X-NU-Partner-Id: org_acme" \
  -H "X-NU-Request-Id: req_playback_0001" \
  -H "Content-Type: application/json" \
  -d '{
    "title_id": "ttl_abc",
    "episode_id": "ep_001",
    "viewer_id_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "country": "KR",
    "device": "web",
    "origin": "https://app.acme.example",
    "preferences": {
      "preferred_languages": ["ko", "en"],
      "preferred_genres": ["romance", "thriller"],
      "subtitle_language": "ko",
      "audio_language": "ko",
      "autoplay_next": true,
      "maturity_rating": "15"
    },
    "expires_in": 1800
  }'
FieldRequiredNotes
title_idYes
episode_idYesMust belong to the title
viewer_id_hashYesPartner-side SHA-256(stable_viewer_id + salt). Production requires exactly 64 lowercase hex characters; Sandbox permits legacy opaque IDs. No raw PII.
countryProduction yes / Sandbox noISO 3166-1 alpha-2. Required in Production and checked against agreement territories. Optional in Sandbox; if present, checked against visible rights territories.
deviceNoFor example: web, ios, android
originConditionalRequired when the key has allowed_origins and must match one of them.
preferencesNoNon-PII viewer preference signals for the hosted webview; see below.
expires_inNoToken TTL in seconds. Default 1,800; absolute maximum 7,200; Production default cap 1,800.

preferences (viewer signals)

Optional object applied by the hosted webview for subtitle/audio defaults, autoplay, and recommendations, and stored on the session for analytics. The schema is strict; unknown keys return 422. Values must contain no raw PII; those containing @ or spaces return 422 validation_failed.

FieldTypeNotes
preferred_languagesstring[] (≤20)BCP-47 or ISO language codes in preference order
preferred_genresstring[] (≤50)Partner-side genre tags
subtitle_languagestringDefault subtitle track
audio_languagestringDefault audio or dub track
autoplay_nextbooleanAutomatically play the next episode
maturity_ratingstringMaximum viewer maturity rating

This endpoint is idempotent through optional X-NU-Request-Id. Retrying the same ID returns the original response; a concurrent duplicate still in progress returns conflict (409).

Response

{
  "data": {
    "playback_session_id": "pbs_123",
    "expires_at": "2026-06-24T00:30:00Z",
    "webview_url": "https://signal-partners.newunivers.ai/w/pbs_123?t=...",
    "manifest": {
      "hls": "https://signal-partners.newunivers.ai/media/hls/pbs_123/signal-city/ep001/master.m3u8?token=...",
      "dash": "https://signal-partners.newunivers.ai/media/dash/pbs_123/signal-city/ep001/manifest.mpd?token=..."
    },
    "tracking": {
      "event_endpoint": "https://signal-partners.newunivers.ai/v1/events",
      "required_events": [
        "EPISODE_STARTED",
        "PLAYBACK_PROGRESS",
        "EPISODE_COMPLETED"
      ]
    }
  }
}

Two equivalent ways play the same authorized session. Choose one.

  • webview_url (recommended): an NU-hosted playback-only page. Open it in a WebView (WKWebView/android.webkit.WebView) or <iframe>. It resolves the manifest internally and applies stored preferences; you need no player integration. The link contains the session ID and signed ?t=; treat it as secret and do not cache it after expires_at.
  • manifest (own player): signed HLS/DASH URLs for partners operating a player. Every manifest or segment request validates signature, TTL, session ID, and approved media path. Source master files are never returned. Validation runs in the app at /media or an equivalent Cloudflare edge worker; the URL contract is identical.

Both methods bind to the same playback_session_id; report events identically for either one.

Test webview

Before obtaining a real session, open the test player to validate WebView or iframe integration. It immediately plays the 9:16 Episode 009 Confession preview without a token, session, or media setup.

https://signal-partners.newunivers.ai/w/test?subtitle=ko&audio=ko&lang=ko&autoplay=1

The test uses exactly the same player as webview_url. If the vertical sample works in your WebView, real links use the same 9:16 display policy.

Validation order

Checks run in order. Missing title or episode returns resource_not_found (404). Once resources resolve, authorization failure returns 403 and records block_reason on a BLOCKED session.

  1. The API key is valid and has playback:token; its environment scopes catalog and session access.
  2. The title is visible in that environment and the episode belongs to it; otherwise resource_not_found.
  3. The rights package is unexpired with status ∈ {CLEAR, RESTRICTED} and apiStreamingAllowed = true.
  4. The episode is READY, with an approved, non-deleted, HLS-ready video asset whose rights are CLEAR or RESTRICTED.
  5. Production: country is required; an ACTIVE agreement covers the title and territory, has production_api_enabled = true, and includes the ready video asset with accessLevel = stream. Sandbox: no agreement is required; when present, country is checked against visible rights territories.
  6. If the key has allowed_origins, origin is required and must match. If allowed_ips is set, the token-issuance request IP must match. These apply only at issuance; the token is not bound to the viewer’s IP.
  7. Concurrency limit: at most PLAYBACK_MAX_CONCURRENT_SESSIONS active sessions (default 3) per viewer_id_hash for the agreement in Production or title in Sandbox. Excess issuance creates a BLOCKED session and returns playback_concurrency_limit (429).

Error reasons

error.codeHTTPCause
resource_not_found404Title is not visible in the environment, or episode is missing or belongs to another title
missing_scope403Key lacks playback:token
license_not_active403No ACTIVE agreement, production_api_enabled is false, or no visible streamable rights package
territory_not_allowed403Production country missing, country outside agreement or rights territories, required origin missing, or origin/IP blocked
episode_not_licensed403Episode is not READY, has no ready video asset, or its asset is outside the licensed set
validation_failed422viewer_id_hash appears to contain raw PII, or preferences contains an unknown key or a value with raw PII ('@' or spaces)
playback_concurrency_limit429Too many active sessions for this viewer and agreement or title
conflict409Concurrent duplicate request using an in-progress X-NU-Request-Id

Sessions

curl "https://signal-partners.newunivers.ai/v1/playback/sessions/pbs_123" \
  -H "Authorization: Bearer nsp_live_xxx" -H "X-NU-Partner-Id: org_acme"
{
  "data": {
    "playback_session_id": "pbs_123",
    "title_id": "ttl_abc",
    "episode_id": "ep_001",
    "status": "issued",
    "block_reason": null,
    "preferences": { "subtitle_language": "ko", "autoplay_next": true },
    "expires_at": "2026-06-24T00:30:00Z",
    "started_at": null,
    "completed_at": null
  }
}

The API returns stored session status in lowercase. Flow: issued → started → completed | expired | revoked | blocked. Token issuance creates issued; EPISODE_STARTED advances it to started and fills started_at; EPISODE_COMPLETED advances it to completed and fills completed_at. Reading past TTL projects expired without persisting on this GET; authorization failure creates blocked with block_reason. Unknown IDs return invalid_playback_session (400).

Revoke a session

curl -X POST "https://signal-partners.newunivers.ai/v1/playback/sessions/pbs_123/revoke" \
  -H "Authorization: Bearer nsp_live_xxx" -H "X-NU-Partner-Id: org_acme"

Marks active issued/started sessions as revoked, removing them from the concurrency count. The response returns the true final state; ended sessions remain completed, expired, or their existing state. The app’s /media path checks revocation on the next manifest request, but an existing edge-worker token may remain valid for its TTL, so keep TTLs short.

viewer_id_hash and expiry

  • viewer_id_hash must be SHA-256 of a stable viewer ID plus a server-held salt. Production accepts exactly 64 lowercase hex characters; Sandbox preserves legacy opaque IDs. NU never accepts or stores raw PII, including in preferences; values containing @ or spaces are rejected.
  • Tokens are short-lived: default 1,800 seconds, absolute maximum 7,200, Production default cap 1,800. Reissue on expiry; never cache webview_url or manifest URLs after expires_at. webview_url contains a signed token, so never log it or put it in a shareable link.
  • Revoking a key or disabling an agreement does not cancel playback tokens already issued. Keep TTLs short and reissue on expiry.

Report playback activity through Event API using the returned playback_session_id.