Authorize viewers for a private stream
The two supported ways to get a private stream's clear rendition ladder instead of the blurred substitute.
A stream created with authentication required (needAuth) does not refuse unauthorized viewers — it serves them a deliberately degraded picture. To give a viewer the real rendition ladder, authorize the manifest request using one of the two paths below.
Prerequisites
- A stream created as private, so the manifest request is subject to authorization.
- The stream's
manifestUrl, read from the stream detail response and passed through verbatim. See Getting the manifest URL. - For Path A: a program-states webhook endpoint already registered and reachable. See Webhooks Overview.
- For Path B: the ability to mint viewer JWTs — your own issuer or the JWT creation endpoint.
Procedure
Pick one path. They are alternatives, not steps: Path A decides per request, Path B decides at mint time.
Path A — an opaque access token your webhook validates
Use this when the decision must be made per viewer, per request, by your own service.
-
Generate a URL-safe opaque token for the viewer. The token must match
^[0-9A-Za-z_+/=-]+$. A JWT does not qualify — it contains.— and neither does an email address. -
Put it on the manifest URL as a query parameter:
https://<manifest-host>/<path>/<streamId>.json?accessToken=<token>noteThe parameter name is a deployment setting, defaulting to
accessToken. If your deployment configures a different name, use that one — the platform does not accept both. -
Answer the
hls-authevent. The manifest service calls your program-states endpoint with the token. The request carries the token underviewTokens— thetoken/viewTokensshape, not theproducing/consumingshape used byprogram-states-v2:{"programs": {"<internal-key>": {"streams": {"<internal-key>": {"viewTokens": [{ "type": "token", "value": "<the token you issued>", "action": "hls-auth" }]}}}}} -
Admit the viewer by mirroring the envelope back with
stop: falseon that token. Judge the request by the token value — the program and stream keys in the envelope are internal identifiers, not the program ID you created the stream with.warningThe response is a fail-closed full mirror: anything you do not echo back is stopped. See Webhook Response Structure.
Path B — a viewer JWT carrying the view-private role
Use this when admission is decided when the token is minted. This is the default path for an SDK integration.
-
Mint the viewer JWT with a top-level
rolesclaim containingview-private:{"sub": "viewer-123","roles": ["view-private"]}cautionroles: ["view-private"]is a top-level claim and is the only thing that grants HLS playback. It is not the same asvideoToken.scopes: ["private-viewer"], which governs WebRTCjoining. Setting the scope without the role leaves playback blurred. -
Present it on the manifest request as
Authorization: Bearer <jwt>. The SDK does this for you when the client is authenticated with that token. -
Expect no webhook. The role is honored directly, before any token validation or webhook call, so neither
hls-authnorjoiningfires. That is by design — the decision was making the token. Use short expiries, because there is no per-request veto.
Result
The manifest response lists the stream's full rendition ladder rather than a single low-resolution entry, and the player renders the clear picture. The media edge re-validates on playlist and segment requests, so on Path A your endpoint sees further calls — batched, so they arrive intermittently rather than once per segment.
Troubleshooting
The picture is blurred and low-resolution, but nothing reported an error
This is what an unauthorized request looks like. When a private stream has a substitute rendition available, an unauthorized manifest request is answered with HTTP 200 carrying only that substitute — a purpose-built obfuscated preview (426×240) that never requires authorization, so preview surfaces can still show a live picture. There is no status code to catch, and the response is indistinguishable from a stream that simply is not producing its main ladder.
Only when no substitute rendition exists does the request fail with 403 Not authorized. Do not build your integration around seeing that 403.
Check, in order: the token matches the character class in Path A step 1; the parameter name matches your deployment's; your webhook admitted the token by mirroring it back.
The webhook never fires
A token that fails the ^[0-9A-Za-z_+/=-]+$ check is rejected before the webhook is called: the substitute is served and your endpoint sees nothing. Because the symptom is silence, this reads as a webhook delivery problem when it is really a token format problem. JWTs and email addresses are the two values most often passed here by mistake.
If you are on Path B, no webhook firing is expected — see step 3.
My handler never sees hls-auth
hls-auth arrives in the token / viewTokens shape shown in Path A step 3. A handler that inspects only the producing / consuming branches of a program-states-v2 payload will not match it and will fall through to its default, which is a fail-closed stop. Handle both shapes — see Webhook Authentication.
Adding ?substitute=false or ?vdc=true changed nothing
Neither is a request parameter, and both are ignored. substitute is a field in the manifest response marking a rendition as the degraded one; vdc is a namespace tag. The only authorization input on a manifest request is the viewer token — the query parameter on Path A, or the bearer JWT on Path B.
Likewise, treat the pt value inside a returned playback location as opaque. It selects internal playback routing. Carry the URL as given; never construct or edit one.
My traffic does not look the way I expected
Three behaviors that are easy to misread:
action: pollingis the producer being re-verified, not a viewer.joiningfires for WebRTC consumers only. It never fires for HLS playback.- HLS playback does not increment
viewCount. Use it as a WebRTC signal, not a total audience count.
Related
- Webhook Authentication — the request and response contracts in full
- Webhooks Overview — which webhook system applies to what
- Player Events & Liveness — what the player reports during playback