Skip to main content

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.

  1. 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.

  2. Put it on the manifest URL as a query parameter:

    https://<manifest-host>/<path>/<streamId>.json?accessToken=<token>
    note

    The 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.

  3. Answer the hls-auth event. The manifest service calls your program-states endpoint with the token. The request carries the token under viewTokens — the token / viewTokens shape, not the producing / consuming shape used by program-states-v2:

    {
    "programs": {
    "<internal-key>": {
    "streams": {
    "<internal-key>": {
    "viewTokens": [
    { "type": "token", "value": "<the token you issued>", "action": "hls-auth" }
    ]
    }
    }
    }
    }
    }
  4. Admit the viewer by mirroring the envelope back with stop: false on 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.

    warning

    The 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.

  1. Mint the viewer JWT with a top-level roles claim containing view-private:

    {
    "sub": "viewer-123",
    "roles": ["view-private"]
    }
    caution

    roles: ["view-private"] is a top-level claim and is the only thing that grants HLS playback. It is not the same as videoToken.scopes: ["private-viewer"], which governs WebRTC joining. Setting the scope without the role leaves playback blurred.

  2. Present it on the manifest request as Authorization: Bearer <jwt>. The SDK does this for you when the client is authenticated with that token.

  3. Expect no webhook. The role is honored directly, before any token validation or webhook call, so neither hls-auth nor joining fires. 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: polling is the producer being re-verified, not a viewer.
  • joining fires 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.