Webhooks
Register a URL and we post to it when something happens, instead of you asking repeatedly whether it has.
Register an endpoint
From Settings → Webhooks, or over the API:
/api/public/v1/webhooks/Body
| Key | Type | Sample | Description |
|---|---|---|---|
| urlrequired | string (https) | https://yourapp.com/hooks/streamos | Where deliveries are posted. Must be https — http is rejected. |
| eventsrequired | string[] | ["VIDEO_TRANSCODING_COMPLETED"] | At least one. See the event reference. |
| fieldsoptional | string[] | ["video.id", "video.hls_url"] | Dot-paths to keep. Omitted or empty delivers the whole payload. |
| descriptionoptional | string | Production ingest worker | For your own reference. Max 200 characters. |
curl -X POST https://api.streamos.in/api/public/v1/webhooks/ \
-H 'Authorization: Bearer sk_live_…' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://yourapp.com/hooks/streamos",
"events": ["VIDEO_TRANSCODING_COMPLETED", "LIVESTREAM_RECORDING_READY"],
"fields": ["video.id", "video.hls_url"]
}'signing_secret shown once. You need it to verify deliveries — store it before closing the response.What you receive
Headers on every delivery
| Key | Type | Sample | Description |
|---|---|---|---|
| X-Streamos-Eventoptional | string | VIDEO_TRANSCODING_COMPLETED | Which event this is, without parsing the body. |
| X-Streamos-Deliveryoptional | string | cmsxp20li0009hvrtokn1yvig | Unique per delivery. Key your processing on it — retries reuse the same id. |
| X-Streamos-Signatureoptional | string | t=1786998983,v1=7daa4782a4f1… | Timestamp and HMAC-SHA256 of the raw body. Verify it. |
{
"event": "VIDEO_TRANSCODING_COMPLETED",
"created_at": "2026-08-17T20:36:23.813Z",
"data": {
"video": {
"id": "oI1j0SfJK3v",
"title": "Lecture 1",
"status": "COMPLETED",
"kind": "UPLOAD",
"duration_seconds": 131,
"size_bytes": "5913969",
"url": "https://…/master.m3u8",
"drm": false
},
"renditions": [
{ "quality": "480p", "url": "https://…/480p/master.m3u8" }
],
"error": null,
"stream": null
}
}streamis null here because this asset was uploaded. On a live stream’s recording it carries the broadcast it came from — { "id": "MflonNqhUg9", "stream_key": "EXSO_1786…" } — where id is the stream’s public id and the only handle /livestreams/{id}/ accepts.
Verify the signature
Anyone can post to your URL. The signature is what proves a delivery came from us.
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(secret, rawBody, header) {
const [, t, v1] = /t=(\d+),v1=([a-f0-9]+)/.exec(header) ?? [];
if (!t || !v1) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
if (expected.length !== v1.length) return false;
return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}express.raw() on this route.The timestamp is inside the signed value, so reject anything more than a few minutes old to prevent replay.
Trimming the payload
fields takes dot-paths and delivers only those. With ["video.id", "video.hls_url"] a delivery is:
{ "video": { "id": "oI1j0SfJK3v", "hls_url": "https://…/master.m3u8" } }Filtering happens when the delivery is built, so a retry cannot leak what the first attempt withheld.
Retries
Respond 2xx to acknowledge. Anything else — or no response within 10 seconds — is retried up to 6 times with growing backoff: roughly 1m, 5m, 25m, 2h, 10h. After that the delivery is abandoned.
Make your handler idempotent
The same X-Streamos-Delivery can arrive more than once — a timeout on our side does not mean your handler did not run. Record the id and ignore repeats.
Acknowledge first, then do the work. A handler that transcodes, uploads or emails before replying will hit the 10-second timeout and be retried while still running.
Listing and removing
/api/public/v1/webhooks//api/public/v1/webhooks/{endpoint_id}/Removing an endpoint stops future deliveries and drops anything queued for retry. Without any endpoint registered every API call still works — you just have to poll for results.