Skip to main content

Webhook Authentication for Streaming

Introduction​

Native Frame's webhook functionality provides a powerful alternative to JWT-based authentication for stream authorization. This guide explains how to use webhooks to authenticate and control streams, offering real-time, dynamic control over stream access and management.

Webhooks are particularly useful when you need constant reference to your system as the source of truth for authentication and authorization decisions. This approach allows for more granular control and real-time updates to stream permissions.

How It Works​

Native Frame's webhook system sends HTTP POST requests to a specified endpoint on your server whenever certain events occur or authentication decisions need to be made. Your server then responds with instructions on how to handle the stream or viewer.

Key Concepts​

  1. Program: A container for one or more streams. Programs allow for bulk operations on multiple streams.
  2. Stream: An individual video stream within a program.
  3. Token: Represents a user's authentication and can be associated with either a broadcaster or a viewer.

To enable the integration, create your streams with authType: "private+program-states". Both the v1 and v2 create-stream endpoints accept the same authType values (public, private, private+program-states) — there is no difference between the API versions here.

Webhook Request Structure​

When Native Frame needs to make an authentication decision, it will send a POST request to your specified endpoint. The request body will contain information about the programs and streams that need authentication.

The body is the program-states payload itself — programs and tokens sit at the top level of the JSON, with no wrapper object around them. Within each stream, tokens are split into producing (broadcasters) and consuming (viewers) arrays, and each token carries the event that fired.

Here's a simplified example of a webhook request:

{
"tokens": [],
"programs": {
"program1": {
"tokens": [],
"streams": {
"stream1": {
"producing": [
{
"streamName": "default",
"token": {
"value": "broadcaster_authkey_123",
"type": "token",
"event": "starting"
}
}
],
"consuming": [
{
"streamName": "default",
"token": {
"value": "viewer_token_456",
"type": "token",
"event": "joining"
}
}
]
}
}
}
}
}
Two fields worth knowing up front
  • For an RTMP broadcast, the producing token's value is the stream's authKey — the ?authKey= portion of the stream key returned by create-stream. Match on it to recognize which broadcaster is starting.
  • streamName is currently always the literal "default" — it is not the name you gave the stream. Identify streams by the programs/streams map keys instead.

Token Events​

The event field on a token indicates what the user is attempting to do. Common events:

  • starting (producing): a broadcast is starting — this is the admission gate. It fires synchronously and fails closed: returning stop: true (or failing to respond) blocks the broadcast before any session is created.
  • creating (producing): a producing token is creating its stream on the gateway.
  • joining (consuming): a viewer is attempting to join a stream for WebRTC playback.
  • hls-auth (top-level token): a viewer is authenticating for HLS playback.
  • polling (producing or consuming): a periodic re-check while the participant is connected — return stop: true to tear them down.
  • format-started / format-ended, manifest-ready, closed, disconnected: lifecycle notifications; no verdict is acted on for these.

Tokens in the top-level tokens array are users not yet associated with a program (for example, requesting to create one).

Detecting which shape you received​

Two request shapes reach the same endpoint, and there is no field that tells you which one you are looking at. Because the body has no {event, id, payload} wrapper, there is no event name to branch on — you must decide from the structure itself:

What the payload containsShapeWhat sends it
producing / consuming arrays, tokens carrying eventprogram-states-v2WebRTC admission, and the RTMP start gate
a single token object plus a viewTokens array, tokens carrying actionlegacy program-statesHLS playback auth (hls-auth)

This is not a new-versus-old distinction that you can age out of: a single current integration receives both, because HLS playback authorization is delivered in the legacy shape today. A handler that recognizes only one shape falls through to its default on the other, and the default is a fail-closed stop — so the users of that transport are rejected with no visible cause.

Branch on the presence of the keys, not on a version you configured:

app.post('/webhook', (req, res) => {
const stream = firstStreamOf(req.body);

if (stream.producing || stream.consuming) {
return res.json(handleV2(req.body)); // WebRTC, RTMP start gate
}
if (stream.token || stream.viewTokens) {
return res.json(handleLegacy(req.body)); // hls-auth
}
// Neither: mirror the request back unchanged rather than answering empty,
// which would stop everything in it.
});

The verdict fields are the same in both shapes (stop, needAuth, appData), and so is the full-mirror rule — only the nesting differs. For the HLS side in full, see Private Stream Viewer Auth.

Webhook Response Structure​

Your server responds with instructions on how to handle each program, stream, and token.

The response is a full mirror — omission means stop

Echo back every program, stream, producing/consuming entry, and token from the request, setting the verdict fields on each. Any entry you leave out of the response is treated as stop: true and torn down — an empty response ({}) stops everything in the request, including your own broadcaster. Only set stop: true deliberately; there is no "ignore" by omission.

Here's the full-mirror response to the example request above (allowing everything and attaching user data):

{
"tokens": [],
"programs": {
"program1": {
"stop": false,
"needAuth": true,
"tokens": [],
"streams": {
"stream1": {
"stop": false,
"needAuth": true,
"producing": [
{
"streamName": "default",
"token": {
"value": "broadcaster_authkey_123",
"event": "starting",
"stop": false,
"appData": {
"user.id": "broadcaster123",
"user.name": "John Doe",
"user.scope": "broadcaster"
}
}
}
],
"consuming": [
{
"streamName": "default",
"token": {
"value": "viewer_token_456",
"event": "joining",
"stop": false,
"appData": {
"user.id": "viewer456",
"user.name": "Jane Smith",
"user.scope": "viewer"
}
}
}
]
}
}
}
}
}

The response body is bare, like the request — no wrapper object.

Key Response Fields​

  • stop (program, stream, or token level): set to true to reject or tear down at that level. Pair with stopReason to record why — it shows up in logs and diagnostics.
  • needAuth (program or stream level): set to true to have future consuming (viewer) tokens forwarded to your webhook for authentication. The program-level flag is the primary switch; the stream-level flag refines it per stream. If needAuth is false, viewer tokens for that program/stream are not sent to you at all.
  • appData (token level): a flat map of string keys to string values for passing user information — dotted keys like user.id, user.name, and user.scope, not nested objects. Keys starting with $sys. are reserved.
Important: WebRTC Scopes and User Identification

When using webhooks with WebRTC streams, it's crucial to set the correct information in the appData field:

Scopes for SFU management:

"appData": {
"user.scope": "broadcaster" // or "viewer", "private-broadcaster", "private-viewer"
}

The user.scope field is essential for the Selective Forwarding Unit (SFU) to properly identify and manage participants. User identification for frontend use:

"appData": {
"user.id": "user123",
"user.name": "John Doe"
}

The user.id and user.name fields are primarily used for identifying users in the frontend application. They don't affect stream permissions but can be useful for displaying user information or managing user interactions in your application's interface.

Using private-broadcaster or private-viewer for user.scope will prevent users with standard broadcaster or viewer JWT scopes from accessing the stream, providing an additional layer of security.

Handling Viewer Tokens​

When a viewer attempts to join a stream using a token that isn't a JWT, Native Frame will pass this token to your webhook for validation. This allows you to implement custom authentication logic for viewers.

Important: Webhook tokens must be unique.

Webhook tokens must be unique and cannot be reused. If a token is reused, the first user who used the token will be kicked from the stream. This includes the broadcaster!

If your webhook response indicates that a viewer should be stopped (by setting stop: true for their token), Native Frame will kick the viewer off the stream.

Stopping Streams​

You can stop an entire stream by setting stop: true in the stream's response object. This is useful for scenarios where you need to immediately terminate a broadcast due to policy violations or other reasons.

Implementing Webhook Authentication​

  1. Set up an endpoint on your server to receive webhook POST requests.
  2. Implement logic to validate tokens and make authorization decisions.
  3. Return a properly structured response indicating whether to allow or deny access, and whether to stop streams or kick viewers.
  4. Configure your Native Frame project to use your webhook endpoint for authentication.

Testing Webhook Integration Locally​

Testing webhook integration locally can be challenging since webhooks require a publicly accessible URL. However, you can use a tool like ngrok to expose your local server to the internet for testing purposes.

  1. Install ngrok from https://ngrok.com/
  2. Start your local server (e.g., on port 3000)
  3. Run ngrok to create a tunnel to your local server:
    ngrok http 3000
  4. ngrok will provide a public URL (e.g., https://1234abcd.ngrok.io)
  5. Configure your Native Frame project to use this ngrok URL as the webhook endpoint
  6. You can now test your webhook integration with your local server

Remember to monitor the ngrok console and your local server logs for incoming requests and any potential errors during testing.

Example Server Implementation

Checkout the Server-Side Setup guide for a simple implementation of a web server that can be used to test webhook authentication.

Best Practices​

  1. Implement proper security measures on your webhook endpoint, such as requesting signature validation.
  2. Ensure your webhook handler responds quickly to avoid timeouts.
  3. Implement proper error handling and logging in your webhook handler.
  4. Use HTTPS for your webhook endpoint to ensure secure communication.
  5. Consider implementing a retry mechanism in case your server is temporarily unavailable.

By leveraging webhook authentication, you can create a highly dynamic and secure streaming experience that integrates seamlessly with your existing user management system. This approach offers real-time control and flexibility that goes beyond what's possible with static JWT authentication.