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:
- Webhooks Overview — the two systems and which you need
- Registering a Webhook — configure one in the dashboard
- Webhook Authentication — verifying deliveries
- Format Availability — the format events in depth
Two kinds of webhook
| Event notifications | Program-states integration | |
|---|---|---|
| Purpose | Tell you something happened | Ask your integration for a verdict |
| Direction | One-way (fire-and-forget) | Request → your response drives behavior |
| Delivery | Async, fanned out, retried (with history) | Synchronous, in the stream lifecycle path |
| Endpoints | Many — you register/manage URLs | Exactly one registered webhook |
| Your response | Ignored (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:
programsandtokensat the top level of the JSON, with no wrapper object around them. There is noeventfield to branch on and noX-Project-IDheader — 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": { … } }, withX-Project-IDandAuthorizationheaders. 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
valueis the stream'sauthKey— the?authKey=portion of the stream key returned by create-stream. Match on it to recognize the broadcaster. streamNameis currently always the literal"default", not the name you gave the stream. Identify streams by theprograms/streamsmap 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.
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.)
| Field | Available at | Effect |
|---|---|---|
stop (+ stopReason) | program, stream, token | Reject or tear down at that level |
needAuth | program, stream | Program level is the primary switch, stream level refines it — if false, consuming (viewer) tokens are not sent to your webhook for that program/stream |
appData | token | Flat 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.
| Event | Scope | Kind | Fires when |
|---|---|---|---|
token-joining | Token | Respondable | A user/token first connects (not yet tied to a program) — gate or attach appData |
token-hls-auth | Token | Respondable | An HLS playback authorization is requested |
token-auth-manifest | Token | Respondable | Program-level manifest access is authorized |
producer-starting | Producer | Respondable | A broadcast is starting — admission gate, return stop to reject before any session is created (fails closed) |
producer-creating | Producer | Respondable | A producing token is creating its stream on the gateway |
producer-polling | Producer | Respondable | Periodic verdict while a producer is live — return stop to tear it down |
consumer-joining | Consumer | Respondable | A viewer starts watching — gate or attach viewer appData. With consumer-closed, the push signal for viewer presence |
consumer-polling | Consumer | Respondable | Periodic verdict while a viewer is connected |
program-state | Program | Respondable | Program-level state/verdict point |
producer-manifest-ready | Producer | Notification | The HLS/DASH manifest is ready |
producer-webrtc-manifest-ready | Producer | Notification | The WebRTC manifest is available |
producer-format-started | Producer | Notification | A transcode variant (encoding) became available — see Format Availability |
producer-format-ended | Producer | Notification | A transcode variant became unavailable |
producer-closed | Producer | Notification | The producer stopped streaming |
producer-disconnected | Producer | Notification | The producer transport/peer dropped |
consumer-closed | Consumer | Notification | A viewer stopped watching — see viewer presence |
consumer-disconnected | Consumer | Notification | A viewer connection dropped |
token-disconnected | Token | Notification | A token left cleanly |
stream-state | Stream | Notification | Stream alias/metadata changed |
program-closed | Program | Notification | The 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-endedtoken events (theeventon the producing token). - Legacy callbacks —
PUT {host}/integration/v1/broadcast/{streamID}/encoding/{format}(begin) andDELETE …(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/"
}
]
}
| Field | Notes |
|---|---|
videoCodec, audioCodec | e.g. h264, aac |
manifest | Viewer playback manifest URL |
location | Base media location |
clientEncoder | Broadcast 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.