Runtime webhook module
Configure outbound webhook delivery and consume every emitted event with its exact payload fields.
Event emission
Every event below requires webhook.enabled=true, a valid delivery configuration and the listed feature or lifecycle condition. Emission is a local handoff to bounded delivery, not a guarantee that the receiver gets the event.
| Event | Additional emission condition | Emitter | Notes |
|---|---|---|---|
stream.started | An enabled push listener accepts a local logical stream, or a managed pull creates one. | Push listener or managed pull controller | Transport reconnects of a managed pull do not create another start. |
stream.stopped | The corresponding local push or managed-pull lifecycle reaches its terminal path. | Push listener or managed pull controller | Carries a closed reason. In legacy mode, repair handover also emits a stop/start pair. |
stream.updated | webhook.streamState.enabled=true and the collector commits a changed summary for an admitted local push or managed pull. | Stream-state collector | A complete replacement, not a delta; changes may be coalesced. No event when the opt-in is disabled. |
recording.started | Enabled recording becomes locally available after its first durable multivariant write. | Recording availability callback | Not emitted just because recording was configured or a session was opened; an empty session emits none. |
recording.stopped | An available recording reaches durable local close with its renditions finalized. | Recording close callback | Not emitted for terminally failed, degraded or incompletely finalized recordings. |
recording.published | Enabled S3 publication completes the durable recording dependency tree and this process writes the publication marker. | S3 publisher callback | Includes recovery completed by this process; reopening an already published session emits nothing. |
recording.failed | The recording session becomes terminally unavailable and its availability is withdrawn or permanently prevented. | Recording terminal-failure callback | A single failed rendition does not emit this event while another rendition keeps the recording available. |
thumbnail.available | webhook.thumbnailAvailable.enabled=true and enabled local thumbnail output durably commits a JPEG and a valid current descriptor. | Thumbnail commit callback | Stale or duplicate callbacks cannot emit availability. This event does not assert S3 publication. |
Stream-state observation covers locally admitted push and managed-pull streams, not cascade input. With stream-state enabled, repair handover preserves the logical instance and produces state updates instead of an intermediate stop/start pair.
Delivery configuration
Each delivery is an HTTP POST to webhook.url with one JSON event and Content-Type: application/json. An optional token is sent as Authorization: Bearer <token>.
| Field under webhook | Default | Purpose |
|---|---|---|
enabled | false | Enables outbound delivery. Disabled configuration starts no dispatcher or outbound requests. |
url | required when enabled | Deployment-supplied receiver URL. |
authToken | empty | Optional bearer token. |
requestTimeout | 5s | Upper bound for each HTTP attempt. |
maxAttempts | 5 | Maximum attempts for an event. |
queueSize | 256 | Lifecycle and recording queue capacity. |
maxConcurrentDeliveries | 4 | Lifecycle and recording delivery concurrency. |
shutdownFlushTimeout | 2s | Bounded queue flush during shutdown. |
streamState.enabled | false | Extends stream start/stop and enables stream.updated. |
thumbnailAvailable.enabled | false | Enables thumbnail.available independently of stream state. |
The state-update and thumbnail dispatchers each have a separate capacity of 64 queued or in-flight events and one HTTP worker. Recording payloads are unchanged by either opt-in; thumbnail payloads do not acquire stream-state fields.
Delivery is best effort with retries and possible duplicates. Success is any 2xx response; network failures, timeouts, 408, 429 and 5xx responses can retry. Other responses are final drops; redirects are not followed. Lifecycle and recording delivery preserves per-stream order, but the separate state and thumbnail lanes have no cross-lane ordering guarantee.
The queue is in memory. Queue saturation, exhausted attempts, shutdown or a process crash can lose events; there is no durable outbox or replay. Consumers that need durable truth must reconcile with the API or object storage.
Common payload and conditional fields
Every event contains the four common fields below. Timestamps use UTC RFC 3339 with exactly millisecond precision, for example 2026-08-18T10:12:13.123Z. Additional fields follow the matrix; absent fields are omitted, not empty strings or null. Nullable fields inside summary follow the separate rules below.
| Field | JSON type | Meaning |
|---|---|---|
eventType | string | One of the eight event names on this page. |
occurredAt | string | Event time, retained unchanged across retries. |
streamId | string | Canonical stream identifier. |
createdTime | string | Logical stream admission time; recovery publication uses the time parsed from the reserved session path. |
recordingUrl | string | Only recording.started, recording.stopped and recording.published: the reserved session's root-relative VOD entry path. |
reason | string | Only stream.stopped and recording.failed: a value from the respective closed vocabulary below. |
schemaVersion | integer | Only opted-in stream events; currently 1. |
instanceId | string | Only opted-in stream events: 32 lowercase hexadecimal epoch characters, a dot and a decimal instance counter. |
revision | string | Only opted-in stream events: a positive decimal revision. Preserve it as a string. |
summary | object | Only opted-in stream.started and stream.updated; a complete state replacement. |
thumbnail | object | Only thumbnail.available; the complete descriptor described below. |
| Event | Common fields | recordingUrl | reason | Additional fields |
|---|---|---|---|---|
stream.started | All four | Absent | Absent | schemaVersion, instanceId, revision, summary when streamState is enabled |
stream.updated | All four | Absent | Absent | schemaVersion, instanceId, revision, summary (requires streamState) |
stream.stopped | All four | Absent | Present | schemaVersion, instanceId, revision when streamState is enabled; never summary |
recording.started | All four | Present | Absent | None |
recording.stopped | All four | Present | Absent | None |
recording.published | All four | Present | Absent | None |
recording.failed | All four | Absent | Present | None |
thumbnail.available | All four | Absent | Absent | thumbnail only (requires thumbnailAvailable) |
Legacy mode: when stream-state mode is off, only stream.started and stream.stopped retain the legacy stream payloads without state fields. stream.updated is not emitted.
Treat recordingUrl as an opaque path. Never build it from createdTime: a reserved recording path can have a collision-adjusted timestamp. Supply the VOD scheme and host from deployment configuration; the origin HTTP listener does not serve /vod/. Recovery publication has no original admission timestamp, so its createdTime may reflect that adjusted path time.
Stream-state summary contract
summary is a full replacement. A missing optional field or an explicit null clears that part of the previous state. It never includes thumbnail state.
| Field within summary | Presence and value |
|---|---|
state | Required string: starting, waitingForKeyframe, healthy, degraded, reconnecting, stopping or stopped. |
source | Required object with kind (push or pull) and transportState. transportState is always present: null for push; for pull, null or dialing, connected, backoff, failed, stopping. |
health | Required object or null before evaluation. An object always contains status, component and reason. Status is healthy, degraded or unavailable; a healthy example is {"status":"healthy","component":"lifecycle","reason":"none"}, not status "ok". |
input, output | Both keys are required, null before discovery/composition; otherwise each is an object with count (integer), truncated (boolean, including false), and tracks (array, at most 16 entries in PMT order). count is the full track count. |
input.tracks[], output.tracks[] | Each object requires kind and codec strings. language, width, height, sampleRate and layer are omitted when empty or zero; language is at most 16 ASCII bytes. Numeric fields describe declared metadata. |
transcode | Required, null while inactive; otherwise an object with profile and reason strings plus video and audio booleans, including false. |
availability | Required object with all five booleans, including false: liveHls, recording, cascadeSrt, stop, removePull. |
truncated | Optional top-level summary boolean: omitted when false, true only for the oversized-start fallback. This differs from the required input/output truncated fields. |
For local stream health, component is lifecycle, source, media or output. Its reason is none, starting, source_connecting, source_reconnecting, source_unreachable, source_no_media, waiting_for_keyframe, missing_idr, media_degraded, output_unavailable, stopping or internal_error, as applicable to the evaluated state.
The escaped JSON body is bounded to 16 KiB. An oversized start retains its identity but uses state: "starting", the known source kind, null transport/health/input/output/transcode fields, all availability flags false and summary.truncated: true. An oversized update is dropped. Stop never carries a summary.
Use (instanceId, revision, eventType) for retry identity. Updates can arrive before start or after stop. Ignore older revisions; retain a terminal tombstone after stop so a delayed update cannot revive the instance. Revisions can skip because updates coalesce. A new process epoch requires explicit resynchronization.
Thumbnail descriptor contract
The thumbnail object has no optional fields. It contains schemaVersion (integer, currently 1), streamId, instanceId, revision, sessionId, fileName, path, contentType, width, height, pts90k and availableAt.
All are strings except schemaVersion, width and height, which are integers. revision is a positive decimal string in the thumbnail domain, independent of stream-state revision; pts90k is a signed decimal string. contentType is image/jpeg, dimensions are positive, and availableAt uses the common UTC millisecond timestamp format.
path is /thumbnails/{sessionId}/{fileName}, without scheme or host. The descriptor is committed only after local JPEG and directory durability and current generation/session checks. Its envelope is also bounded to 16 KiB. It carries no top-level schemaVersion, instanceId, revision or summary.
Terminal reason values
stream.stopped: transport_disconnect, transport_error, peer_idle_timeout, media_timeout, invalid_input, resource_limit, event_queue_overflow, worker_panic, shutdown, internal_error, api_stop or managed-pull retry_deadline. Legacy repair handover also emits repair_hand_over; stream-state mode suppresses that intermediate stop.
recording.failed: queue_full, session_cap, low_water, disk_full, write_timeout, write_error, permission_denied, invalid_config or rendition_rejected. These are closed reason codes, never raw errors.
stream.started
With webhook.enabled=true, an enabled push listener accepting a local logical stream or a managed pull creating one emits this event. Managed-pull transport reconnects do not create another start.
This is the legacy start payload. With webhook.streamState.enabled=true, it also contains schemaVersion, instanceId, revision and the complete summary described above.
{
"eventType": "stream.started",
"occurredAt": "2026-08-18T10:12:13.123Z",
"streamId": "tenant:channel-123",
"createdTime": "2026-08-18T10:12:12.900Z"
}
stream.updated
With both webhook.enabled=true and webhook.streamState.enabled=true, the collector emits this event when it commits a changed summary for an admitted local push or managed pull. The payload is a complete replacement; changes may be coalesced.
{
"eventType": "stream.updated",
"occurredAt": "2026-08-18T10:20:00.321Z",
"streamId": "tenant:channel-123",
"createdTime": "2026-08-18T10:12:12.900Z",
"schemaVersion": 1,
"instanceId": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6.42",
"revision": "5",
"summary": {
"state": "healthy",
"source": {
"kind": "push",
"transportState": null
},
"health": {
"status": "healthy",
"component": "lifecycle",
"reason": "none"
},
"input": {
"count": 1,
"truncated": false,
"tracks": [
{
"kind": "video",
"codec": "h264",
"width": 1920,
"height": 1080
}
]
},
"output": {
"count": 1,
"truncated": false,
"tracks": [
{
"kind": "video",
"codec": "h264",
"width": 1920,
"height": 1080
}
]
},
"transcode": null,
"availability": {
"liveHls": true,
"recording": true,
"cascadeSrt": false,
"stop": true,
"removePull": false
}
}
}
stream.stopped
With webhook.enabled=true, this event reports the terminal path of the corresponding local push or managed-pull lifecycle. Legacy repair handover also emits a stop/start pair; stream-state mode suppresses that intermediate stop.
This is the legacy stop payload. Stream-state opt-in adds schemaVersion, instanceId and revision, but never summary.
{
"eventType": "stream.stopped",
"occurredAt": "2026-08-18T10:31:02.481Z",
"streamId": "tenant:channel-123",
"createdTime": "2026-08-18T10:12:12.900Z",
"reason": "media_timeout"
}
recording.started
With webhook.enabled=true and recording enabled, this event is emitted when the recording first becomes locally available after its first durable multivariant write. Opening an empty recording session does not emit it.
{
"eventType": "recording.started",
"occurredAt": "2026-08-18T10:12:19.004Z",
"streamId": "tenant:channel-123",
"createdTime": "2026-08-18T10:12:12.900Z",
"recordingUrl": "/vod/2026/08/18/tenant%3Achannel-123/101212900/index.m3u8"
}
recording.stopped
With webhook.enabled=true, an available recording emits this event on durable local close with its renditions finalized. A terminally failed, degraded or incompletely finalized recording does not emit it.
{
"eventType": "recording.stopped",
"occurredAt": "2026-08-18T10:30:12.500Z",
"streamId": "tenant:channel-123",
"createdTime": "2026-08-18T10:12:12.900Z",
"recordingUrl": "/vod/2026/08/18/tenant%3Achannel-123/101212900/index.m3u8"
}
recording.published
With webhook.enabled=true and S3 publication enabled, this event is emitted when this process completes the durable recording dependency tree and writes the publication marker. Recovery completed by this process can emit it; reopening an already published session cannot.
{
"eventType": "recording.published",
"occurredAt": "2026-08-18T10:33:42.501Z",
"streamId": "tenant:channel-123",
"createdTime": "2026-08-18T10:12:12.900Z",
"recordingUrl": "/vod/2026/08/18/tenant%3Achannel-123/101212900/index.m3u8"
}
recording.failed
With webhook.enabled=true, this event is emitted when a recording becomes terminally unavailable and its availability is withdrawn or permanently prevented. A single failed rendition does not emit it while another keeps the recording available. This example is a separate failed recording outcome, not a continuation of the successfully published example.
{
"eventType": "recording.failed",
"occurredAt": "2026-08-18T10:14:31.900Z",
"streamId": "tenant:channel-123",
"createdTime": "2026-08-18T10:12:12.900Z",
"reason": "session_cap"
}
thumbnail.available
With both webhook.enabled=true and webhook.thumbnailAvailable.enabled=true, enabled local thumbnail output emits this event after durably committing a JPEG and a valid current descriptor. Stale or duplicate callbacks cannot emit it; the event does not assert S3 publication.
{
"eventType": "thumbnail.available",
"occurredAt": "2026-08-30T12:00:00.000Z",
"streamId": "demo",
"createdTime": "2026-08-30T12:00:00.000Z",
"thumbnail": {
"schemaVersion": 1,
"streamId": "demo",
"instanceId": "aabbccddeeff00112233445566778899.1",
"revision": "3",
"sessionId": "2026/08/30/demo/120000000",
"fileName": "000000000001-00000000000000090000.jpg",
"path": "/thumbnails/2026/08/30/demo/120000000/000000000001-00000000000000090000.jpg",
"contentType": "image/jpeg",
"width": 400,
"height": 225,
"pts90k": "90000",
"availableAt": "2026-08-30T12:00:00.000Z"
}
}
Configuration reference
The configuration document rules apply to every runtime module. The complete field reference covers every accepted option; the maximal composition shows how modules fit together.