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:

POST/api/public/v1/webhooks/

Body

KeyTypeSampleDescription
urlrequiredstring (https)https://yourapp.com/hooks/streamosWhere deliveries are posted. Must be https — http is rejected.
eventsrequiredstring[]["VIDEO_TRANSCODING_COMPLETED"]At least one. See the event reference.
fieldsoptionalstring[]["video.id", "video.hls_url"]Dot-paths to keep. Omitted or empty delivers the whole payload.
descriptionoptionalstringProduction ingest workerFor your own reference. Max 200 characters.
curl
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"]
  }'
The response contains a signing_secret shown once. You need it to verify deliveries — store it before closing the response.

What you receive

Headers on every delivery

KeyTypeSampleDescription
X-Streamos-EventoptionalstringVIDEO_TRANSCODING_COMPLETEDWhich event this is, without parsing the body.
X-Streamos-Deliveryoptionalstringcmsxp20li0009hvrtokn1yvigUnique per delivery. Key your processing on it — retries reuse the same id.
X-Streamos-Signatureoptionalstringt=1786998983,v1=7daa4782a4f1…Timestamp and HMAC-SHA256 of the raw body. Verify it.
POST /hooks/streamos
{
  "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.

node
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));
}
Use the raw request body, not a re-serialised object. Re-encoding changes the bytes — key order, whitespace — and the digest will never match. In Express that means 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:

json
{ "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

GET/api/public/v1/webhooks/
DELETE/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.