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
| Method | Path | Purpose |
|---|---|---|
| POST | /playback/tokens | Authorize an episode → webview link plus signed manifest |
| GET | /playback/sessions/{session_id} | Get a playback session |
| POST | /playback/sessions/{session_id}/revoke | End 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
}'| Field | Required | Notes |
|---|---|---|
title_id | Yes | |
episode_id | Yes | Must belong to the title |
viewer_id_hash | Yes | Partner-side SHA-256(stable_viewer_id + salt). Production requires exactly 64 lowercase hex characters; Sandbox permits legacy opaque IDs. No raw PII. |
country | Production yes / Sandbox no | ISO 3166-1 alpha-2. Required in Production and checked against agreement territories. Optional in Sandbox; if present, checked against visible rights territories. |
device | No | For example: web, ios, android |
origin | Conditional | Required when the key has allowed_origins and must match one of them. |
preferences | No | Non-PII viewer preference signals for the hosted webview; see below. |
expires_in | No | Token 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.
| Field | Type | Notes |
|---|---|---|
preferred_languages | string[] (≤20) | BCP-47 or ISO language codes in preference order |
preferred_genres | string[] (≤50) | Partner-side genre tags |
subtitle_language | string | Default subtitle track |
audio_language | string | Default audio or dub track |
autoplay_next | boolean | Automatically play the next episode |
maturity_rating | string | Maximum 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 storedpreferences; you need no player integration. The link contains the session ID and signed?t=; treat it as secret and do not cache it afterexpires_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/mediaor 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=1The 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.
- The API key is valid and has
playback:token; its environment scopes catalog and session access. - The title is visible in that environment and the episode belongs to it; otherwise
resource_not_found. - The rights package is unexpired with
status ∈ {CLEAR, RESTRICTED}andapiStreamingAllowed = true. - The episode is
READY, with an approved, non-deleted, HLS-ready video asset whose rights areCLEARorRESTRICTED. - Production:
countryis required; anACTIVEagreement covers the title and territory, hasproduction_api_enabled = true, and includes the ready video asset withaccessLevel = stream. Sandbox: no agreement is required; when present,countryis checked against visible rights territories. - If the key has
allowed_origins,originis required and must match. Ifallowed_ipsis set, the token-issuance request IP must match. These apply only at issuance; the token is not bound to the viewer’s IP. - Concurrency limit: at most
PLAYBACK_MAX_CONCURRENT_SESSIONSactive sessions (default 3) perviewer_id_hashfor the agreement in Production or title in Sandbox. Excess issuance creates aBLOCKEDsession and returnsplayback_concurrency_limit(429).
Error reasons
| error.code | HTTP | Cause |
|---|---|---|
resource_not_found | 404 | Title is not visible in the environment, or episode is missing or belongs to another title |
missing_scope | 403 | Key lacks playback:token |
license_not_active | 403 | No ACTIVE agreement, production_api_enabled is false, or no visible streamable rights package |
territory_not_allowed | 403 | Production country missing, country outside agreement or rights territories, required origin missing, or origin/IP blocked |
episode_not_licensed | 403 | Episode is not READY, has no ready video asset, or its asset is outside the licensed set |
validation_failed | 422 | viewer_id_hash appears to contain raw PII, or preferences contains an unknown key or a value with raw PII ('@' or spaces) |
playback_concurrency_limit | 429 | Too many active sessions for this viewer and agreement or title |
conflict | 409 | Concurrent 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_hashmust 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 inpreferences; 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_urlor manifest URLs afterexpires_at.webview_urlcontains 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.