Skip to content

Webhooks and a Public Event Catalog

Until now, a FrameWorks integration that needed to react to a stream going live or a clip becoming ready had two options: keep a GraphQL subscription open, or poll. Neither works well for a serverless function or a backend that restarts during deploys. Webhooks are the missing piece, and before we could send them we needed a set of events that is safe to hand to a third-party server and stable enough to write code against.

Both now exist. Your account’s events are published as a versioned catalog, available live over a subscription and as signed HTTPS requests to endpoints you register.

An event is a promise about something that already happened

Section titled “An event is a promise about something that already happened”

The event catalog covers streams going live or idle, stream keys being rotated, clips, recordings and uploads becoming ready or failing, multistream target status, API tokens, billing, account suspension, and custom domains. Each type has a protobuf message in a versioned public package, and that message is the payload.

Two rules make the catalog safe to build on. First, an event is written in the same database transaction as the change it describes. A billing.invoice_paid event exists only if the invoice really is paid; there is no window where your backend hears about a change that later rolled back. Second, a payload carries only identifiers and values your account already sees through the API. It does not carry internal stream names, node IDs, storage paths, IP addresses, or raw error text; a failed clip reports a coarse reason rather than a stack trace.

Within the v1 package fields are only added. A breaking change ships as a v2 message, and each webhook endpoint stays on the version it was created with, so a receiver you wrote today keeps parsing what it receives.

The same events are available over the tenantEvents GraphQL subscription, filtered by type and stream. The dashboard’s media library uses it to refresh when an artifact finishes processing.

Under Developer → Webhooks you add HTTPS endpoints and pick the event types each one receives. Each event becomes a POST:

webhook-id: 0199a3c2-7e41-7d2a-9b3e-5f8c1d2e4a67
webhook-timestamp: 1789826608
webhook-signature: v1,NSZG8yLlem8faHh7EcOnDIbFIp4Wa/3hhM320Atl5LM=
{"id":"0199a3c2-7e41-7d2a-9b3e-5f8c1d2e4a67","type":"clip.ready","api_version":"v1","created_at":"2026-09-19T14:03:27.412Z","data":{"artifact":{"artifactId":"c7f3a9e2b41d4f6a8e0b5d2c9a1f7e34","kind":"ARTIFACT_KIND_CLIP","streamId":"5b0e6a2c-3f1d-4c8e-9a7b-2d4f6e8a0c13","playbackId":"k3v9x2m7q4w8"},"durationMs":"30000","sizeBytes":"7340032"}}

That is a real request body and signature, produced by the delivery code with the example secret whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw. The data object uses the protobuf JSON mapping, so 64-bit values such as sizeBytes are strings.

The signature follows the Standard Webhooks specification: HMAC-SHA256 over <webhook-id>.<webhook-timestamp>.<body>. We chose it over a scheme of our own because verification libraries already exist for most languages. In Node:

import { Webhook } from "standardwebhooks";
const event = new Webhook(process.env.FRAMEWORKS_WEBHOOK_SECRET!).verify(rawBody, headers);

We checked the example above against the official TypeScript, Go, and Python libraries; all three accept it. The timestamp is part of the signed content, so a captured request cannot be replayed against you days later.

The webhook-id is the event ID, and it stays the same across every attempt, every replay, and every endpoint. That is the one field a receiver needs to handle duplicates: store the IDs you have processed and ignore a repeat. Failed deliveries are retried for about three days, every attempt is logged for 30 days, and you can replay one delivery or a whole time range after an outage on your side; the webhooks guide has the schedule and the replay calls.

Delivery runs apart from the systems that validate stream keys and serve playback. A customer endpoint that hangs or returns errors delays its own deliveries and nobody else’s.

Endpoint URLs must resolve to public addresses, and that check runs again on every connection. A DNS record changed to point at an internal address after the endpoint was created is refused at send time, so a webhook cannot be turned into a way to reach our private network.

The limit worth knowing up front: an endpoint receives events from the moment it is created, with no backfill, and events carry no ordering guarantee across retries. Design the receiver to look up current state when order matters.

The webhooks guide covers endpoints, verification in TypeScript, Go, and Python, retries, auto-disable, secret rotation, and replay. The events reference lists every event type, when it fires, and its payload fields. Agents connected over MCP can list, test, and replay deliveries with an explicit confirmation.