Skip to main content

Webhooks

Native Frame delivers events to your webhook endpoint. This page is the technical reference — the two webhook systems, payload formats, the response contract, and the full event catalog. For setup and how-to, see the prose guides:

Two kinds of webhook​

Event notificationsProgram-states integration
PurposeTell you something happenedAsk your integration for a verdict
DirectionOne-way (fire-and-forget)Request → your response drives behavior
DeliveryAsync, fanned out, retried (with history)Synchronous, in the stream lifecycle path
EndpointsMany — you register/manage URLsExactly one registered webhook
Your responseIgnored (only HTTP status matters)stop / needAuth / appData are acted on

Notification endpoints are managed in the dashboard or via the Events API (GET / PUT / DELETE /events/v2/webhook/{name}). The program-states integration is enabled by creating streams with authType: "private+program-states" — both the v1 and v2 create-stream endpoints accept the same authType values (public, private, private+program-states). See Webhooks Overview for the full comparison.

Delivery formats​

Program-states events are delivered as a nested payload — program → stream → producing/consuming token. What your endpoint receives depends on how you integrate:

  • Dashboard-registered webhook (standard) — deliveries arrive via the events service, and the body is the bare payload: programs and tokens at the top level of the JSON, with no wrapper object around them. There is no event field to branch on and no X-Project-ID header — your registered endpoint already identifies the project.
  • Direct integration (auth service POSTs straight to your endpoint) — the same payload arrives wrapped in an envelope { "event": "program-states-v2", "id": "<uuid>", "payload": { … } }, with X-Project-ID and Authorization headers. The flat format below is also direct-only.

In both cases your response is the bare response payload — never wrap it in an envelope.

Nested payload (standard)​

State is nested program → stream → token, and each token carries the event that fired. tokens at the top level are users not yet associated with a program (e.g. requesting to create one). This is the body exactly as a dashboard-registered endpoint receives it:

{
"tokens": [],
"programs": {
"<programID>": {
"tokens": [],
"streams": {
"<streamID>": {
"producing": [
{
"streamName": "default",
"token": { "type": "token", "value": "<authKey>", "event": "starting" }
}
],
"consuming": [
{
"streamName": "default",
"token": { "type": "token", "value": "<viewerToken>", "event": "joining" }
}
]
}
}
}
}
}

Two field notes:

  • 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 the broadcaster.
  • streamName is currently always the literal "default", not the name you gave the stream. Identify streams by the programs/streams map keys.

Flat — event: "events" (direct integration only)​

An alternative flat shape — a list of { id, type, payload } with no nesting (id is server-generated for response correlation). It's configured per project in the auth service and delivered only via direct integration; it is not available through the events-service webhook, and only a few integrations use it today. Most integrators use the nested shape above.

{
"event": "events",
"payload": {
"events": [
{ "id": "evt-1", "type": "producer-format-started", "payload": { "encodings": [ /* … */ ] } }
]
}
}

The response contract​

For respondable events your reply drives platform behavior.

The nested 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 omit is treated as stop: true ("not returned from integration") and torn down — an empty response stops everything in the request, including your own broadcaster. There is no "ignore" by omission. (Only the flat direct-integration format has sparse semantics, where omitted control-surface events default to stop=false.)

FieldAvailable atEffect
stop (+ stopReason)program, stream, tokenReject or tear down at that level
needAuthprogram, streamProgram level is the primary switch, stream level refines it — if false, consuming (viewer) tokens are not sent to your webhook for that program/stream
appDatatokenFlat map of string keys/values attached to the token (dotted keys such as user.id, user.name, user.scope; $sys. prefix reserved)

A full-mirror response to the request above, admitting the broadcaster and rejecting the viewer:

{
"tokens": [],
"programs": {
"<programID>": {
"stop": false,
"needAuth": true,
"tokens": [],
"streams": {
"<streamID>": {
"stop": false,
"needAuth": true,
"producing": [
{ "streamName": "default",
"token": { "value": "<authKey>", "event": "starting", "stop": false,
"appData": { "user.scope": "broadcaster" } } }
],
"consuming": [
{ "streamName": "default",
"token": { "value": "<viewerToken>", "event": "joining", "stop": true, "stopReason": "not entitled" } }
]
}
}
}
}
}

Event catalog​

Customer-facing events. Respondable = your response (stop/needAuth/appData) is acted on; Notification = informational only.

Naming: in the nested shape (what dashboard-registered endpoints receive) the per-token event is the bare name — the scope comes from where the token sits (producing[], consuming[], or tokens[]), so producer-starting arrives as event: "starting" on a producing token. The prefixed names below appear literally only as the type field of the flat direct-integration format.

EventScopeKindFires when
token-joiningTokenRespondableA user/token first connects (not yet tied to a program) — gate or attach appData
token-hls-authTokenRespondableAn HLS playback authorization is requested
token-auth-manifestTokenRespondableProgram-level manifest access is authorized
producer-startingProducerRespondableA broadcast is starting — admission gate, return stop to reject before any session is created (fails closed)
producer-creatingProducerRespondableA producing token is creating its stream on the gateway
producer-pollingProducerRespondablePeriodic verdict while a producer is live — return stop to tear it down
consumer-joiningConsumerRespondableA viewer starts watching — gate or attach viewer appData. With consumer-closed, the push signal for viewer presence
consumer-pollingConsumerRespondablePeriodic verdict while a viewer is connected
program-stateProgramRespondableProgram-level state/verdict point
producer-manifest-readyProducerNotificationThe HLS/DASH manifest is ready
producer-webrtc-manifest-readyProducerNotificationThe WebRTC manifest is available
producer-format-startedProducerNotificationA transcode variant (encoding) became available — see Format Availability
producer-format-endedProducerNotificationA transcode variant became unavailable
producer-closedProducerNotificationThe producer stopped streaming
producer-disconnectedProducerNotificationThe producer transport/peer dropped
consumer-closedConsumerNotificationA viewer stopped watching — see viewer presence
consumer-disconnectedConsumerNotificationA viewer connection dropped
token-disconnectedTokenNotificationA token left cleanly
stream-stateStreamNotificationStream alias/metadata changed
program-closedProgramNotificationThe program/stream ended

Format availability (Format Begin / Format End)​

producer-format-started (Format Begin) and producer-format-ended (Format End) fire as a transcode variant becomes available / unavailable. They can be delivered two ways — both carry the same payload, and the broadcast is identified by streamID:

  • Program-states webhook — as format-started / format-ended token events (the event on the producing token).
  • Legacy callbacks — PUT {host}/integration/v1/broadcast/{streamID}/encoding/{format} (begin) and DELETE … (end), where Native Frame calls an endpoint you host.

The Format Begin payload:

{
"videoCodec": "h264",
"audioCodec": "aac",
"manifest": "https://cdn.example.com/manifest.m3u8",
"location": "https://cdn.example.com/",
"clientEncoder": "obs",
"encodings": [
{
"isOriginal": true,
"videoWidth": 1280, "videoHeight": 720,
"videoKbps": 2500, "audioKbps": 128,
"videoPts": 12478, "audioPts": 12478,
"collected": 1718040000000,
"location": "https://cdn.example.com/720p/"
}
]
}
FieldNotes
videoCodec, audioCodece.g. h264, aac
manifestViewer playback manifest URL
locationBase media location
clientEncoderBroadcast encoder, e.g. obs
encodings[]One per variant: isOriginal, videoWidth/videoHeight, videoKbps/audioKbps, videoPts/audioPts, collected (unix millis), location

Format End carries only the streamID — the encoding is identified by the callback path ({format}) or the surrounding token context.

See the Format Availability guide for both delivery methods, the response shape, and migration notes.

See also​