A video platform processing 40,000 uploads a day noticed something strange. Assets were finishing encode. The dashboard showed green. But users kept complaining that videos stayed stuck on "Processing..." for an hour before playing. The support queue filled up. The engineering team blamed the encoder.
The encoder was fine. The webhook handler was crashing on a malformed payload once every forty minutes, silently killing the process, and no one had alerts on the consumer. Every retry landed on the same broken container. Users saw stale state because the backend never heard the video.media.ready event.
This is what webhooks look like in production in 2026. Not a toy integration. A critical path that your video pipeline depends on for state propagation. Get them wrong and your app lies to users. Get them right and you stop polling, stop guessing, and start reacting in real time.
Video webhooks push HTTP events from your video API to your backend the moment something happens: an upload completes, an encode finishes, a live stream goes active, a subtitle track is ready. They replace polling, cut latency, and let your app react instead of checking. To use them safely you need to subscribe to the right event types, verify every signature with HMAC-SHA256, handle retries with idempotency keys, and monitor delivery like any other critical pipeline. This guide covers the event taxonomy, the security steps, the retry semantics, and two real automation patterns you can ship this week.
A webhook is an HTTP POST that a service sends to a URL you own when an event happens. You register the URL once. The service fires a request every time a matching event occurs. Your server parses the payload, does something useful, and returns a 2xx to acknowledge.
The difference between a webhook and an API call is direction. You call an API when you want data. A webhook calls you when data changes. For video pipelines this matters because encoding, live streaming, and AI processing are asynchronous by nature. An upload can take seconds or minutes. A live stream can go active at any moment. Polling every five seconds to check status wastes compute on both sides and still adds lag. A webhook delivers the answer within milliseconds of the event.
A live stream has a lifecycle, and every stage of that lifecycle fires a webhook. The sequence starts when your app creates a stream object via the API. A video.live_stream.created event hits your backend with the stream key and RTMP ingest URL. Your UI displays a "waiting for broadcaster" state.
The broadcaster connects over RTMP. Your backend receives video.live_stream.rtmp_detected first, signaling the ingest connection. Then video.live_stream.active fires once the stream is fully live. Your UI flips from "offline" to "live." Viewers join. When the broadcaster disconnects, video.live_stream.disconnected fires. Your app shows "stream ended" and kicks off VOD processing for the recorded segment.
This replaces polling entirely. Without webhooks, your frontend would need to hit a "get stream status" endpoint every few seconds. Multiply that by thousands of concurrent viewers checking if the stream is live, and you've built yourself a self-inflicted DDoS. Webhooks invert the model: the video API tells you when state changes, and you react once.
The practical pattern looks like a state machine in your webhook handler. Map video.live_stream.created to an "offline" state in your database. Map video.live_stream.rtmp_detected to "connecting." Map video.live_stream.active to "live." Map video.live_stream.disconnected to "ended." Your frontend reads this state and renders the right UI. Each transition is a single database write triggered by a single POST. No timers. No cron jobs. No wasted API calls asking "is it live yet?"
When VOD processing finishes after the stream ends, a separate video.media.ready webhook arrives with the playback URL. Your handler writes it to the same stream record and the replay becomes available. The entire lifecycle, from stream creation to VOD playback, runs on five webhook events.
Not every video event is worth subscribing to. Most engineering teams overcommit on day one and drown in noise by week two. The useful default is a tight set of nine events that cover upload, encoding, live streaming, and AI tracks.
Two rules keep event handling sane. Subscribe only to events your app actually reacts to. Every subscription you don't consume adds retry load on your endpoint for zero gain. And treat event names as a contract: once your handler switches on video.media.ready, renaming it in config becomes a breaking change across every consumer.
Your webhook URL is a public HTTP endpoint. Anyone can POST to it. Without signature verification, an attacker can fake a video.media.ready event for an asset they don't own and trick your backend into marking it playable, billing the wrong user, or triggering downstream workflows. An ongoing audit of more than 100 leading API providers found a 17% year-over-year jump in HMAC-SHA256 signature adoption (Svix State of Webhooks 2024), and the trend has only accelerated into 2026.
The fix is an HMAC signature sent in a request header. The video API signs the raw request body with a shared secret using HMAC-SHA256 before sending. Your server recomputes the same HMAC with the same secret and compares. If the signatures match, the event is authentic. If they don't, reject with a 401.
Here is a Node.js verification using the FastPix-Signature header:
const crypto = require('crypto');
function verifyWebhook(req, secret) {
const signature = req.headers['fastpix-signature'];
const rawBody = req.rawBody; // raw bytes, not parsed JSON
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const sigBuffer = Buffer.from(signature, 'hex');
const expBuffer = Buffer.from(expected, 'hex');
if (sigBuffer.length !== expBuffer.length) return false;
return crypto.timingSafeEqual(sigBuffer, expBuffer);
}The same check in Python:
import hmac
import hashlib
def verify_webhook(headers, raw_body, secret):
signature = headers.get('FastPix-Signature', '')
expected = hmac.new(
secret.encode('utf-8'),
raw_body, # bytes, not parsed dict
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)Three details that break this in production. Always sign the raw body bytes. If your framework parses JSON before you access the body, the re-serialized string will have different whitespace and the signature will never match. Always use constant-time comparison (timingSafeEqual in Node, hmac.compare_digest in Python) to avoid timing attacks. And store webhook secrets in your secret manager, not your environment file committed to git.
Every webhook provider retries failed deliveries. Yours will too. The contract is simple: if your endpoint returns 2xx within a timeout window, the delivery succeeds. If it returns 5xx, times out, or throws a connection error, the provider retries with exponential backoff. After a fixed number of failed attempts the event is marked permanently failed and dropped.
This creates two realities your code has to handle.
Duplicates happen. A flaky network can make you return 200 on delivery one, time out on delivery two, and receive delivery three before your handler finishes writing the database row from delivery one. Your database now has two rows for the same event. The fix is an idempotency key on every webhook event (most providers include a unique event_id). Store it in a dedupe table with a unique index. If you see the same event_id twice, return 200 without reprocessing.
Outages drop events. If your consumer goes down for longer than the retry window, events are gone. Design a backfill path. On startup, your consumer should query the video API for any assets with terminal states (ready, failed) created in the last N hours and reconcile state. A webhook is a push optimization; the API is the source of truth.
Return the right status codes. 2xx acknowledges receipt. 4xx tells the provider the event is malformed and to stop retrying (use this sparingly; if it's really a bug on your end, 5xx lets you retry after a deploy). 5xx triggers retries. Anything slower than your provider's timeout (typically 10-30 seconds) is treated as a failure, even if you eventually respond 200.
Live streaming platforms depend on webhooks for two things that polling cannot deliver fast enough: stream health monitoring and live-to-VOD transitions.
Stream health events fire when something goes wrong mid-broadcast. A bitrate drop below a threshold, an encoder disconnect, a CDN endpoint going unhealthy. Your backend catches these events and updates a control room dashboard in real time. Without webhooks, your monitoring UI would need to poll stream health every second per active stream. At 500 concurrent streams, that is 500 requests per second just to show a status badge.
The live-to-VOD transition is where webhook chaining earns its keep. When a broadcaster disconnects, video.live_stream.disconnected fires. Your handler catches it and initiates VOD processing on the recorded segment. When encoding finishes, video.media.ready arrives with the replay playback URL. Two webhooks, zero polling, and the replay is available within minutes of the broadcast ending.
Simulcast adds another layer. If your platform pushes to multiple CDN endpoints simultaneously, each endpoint can fire its own status events. Your webhook consumer tracks which endpoints are healthy and which dropped. This is the kind of operational visibility that turns a live streaming product from "it mostly works" into "we know exactly what is happening at every edge."
User-generated content platforms process millions of uploads daily. Every upload triggers a chain: file received, content moderation runs, encoding starts, thumbnail generates, AI tags the content, and the video publishes to the feed. Each step fires its own webhook. The architecture is a fan-out pattern where one upload event spawns five or six downstream events.
The content moderation webhook is the gatekeeper. When a user uploads a video, the platform runs it through a moderation pipeline. If the content passes, a "moderation.approved" event fires and encoding begins. If it fails, a "moderation.rejected" event fires and the upload gets flagged. Without this webhook, the encoding pipeline would process every upload regardless of content policy violations, wasting compute on videos that will never publish.
The volume challenge is real. A UGC platform with 2 million daily uploads generates roughly 10 million webhook events per day across the full processing chain. Your webhook consumers need to handle burst traffic during peak upload hours without dropping events. This means horizontal scaling on the consumer side, message queues between the webhook endpoint and the actual processing logic, and idempotency keys on every handler.
Short-form video apps optimize the upload-to-publish pipeline to stay under 60 seconds. Webhooks make this possible by eliminating the polling lag between each processing stage. The moment encoding finishes, the publish handler fires. The moment AI tagging completes, the recommendation engine indexes the video. Each handoff is a webhook, not a timer.
Twitch's EventSub is a subscription-based webhook system covering every event type on the platform. You subscribe to specific events like stream.online, stream.offline, and channel.update and Twitch pushes them to your endpoint when they fire.
EventSub supports three transport modes from the same subscription schema: HTTPS webhooks for server-to-server integrations, WebSockets for client apps without a public endpoint, and Conduits for high-scale apps that shard event delivery across multiple consumers. All three share the same event envelope and signature format. Write one parser, use any transport.
The Conduit architecture is where it gets interesting for scale. A single app can run up to 5 conduits with 20,000 shards each, distributing webhook delivery across consumers for horizontal scaling (Twitch EventSub Conduit docs). For apps tracking thousands of streamers simultaneously, this is the difference between a webhook consumer that keeps up and one that falls behind during peak hours.
YouTube uses WebSub (formerly PubSubHubbub) for channel-level notifications. When a channel publishes a new video or starts a live stream, subscribers receive an Atom feed update at their registered callback URL.
The registration flow works like this: your app sends a subscription request to YouTube's hub with a hub.callback URL, a hub.topic (the channel's feed URL), and a lease duration. YouTube verifies the callback with a GET challenge containing a hub.verify_token. Once verified, YouTube POSTs Atom XML entries to your callback whenever the channel publishes.
The limitation is granularity. YouTube's webhook system tells you "a new video was published" or "a stream started." It does not tell you "encoding finished," "a specific rendition is ready," or "a caption track was processed." If you need video infrastructure events like encoding status, track readiness, or playback quality changes, YouTube's webhooks won't cover it. You would need a video-native API with a deeper event taxonomy for those workflows.
Instagram's Graph API provides webhooks for media events through Meta's webhook infrastructure. Apps subscribe to receive notifications when new media is posted, including Reels and video content.
The setup follows Meta's standard verification handshake. Instagram sends a GET request to your callback URL with a hub.challenge parameter. Your endpoint returns the challenge value to confirm ownership. After verification, Instagram POSTs event payloads when subscribed events fire, like new media published or comments added.
The webhook paradigm here is different from video infrastructure APIs. Instagram webhooks are designed for social media management tools. You get content metadata events: "new post published," "comment added," "mention received." You do not get encoding events, playback quality events, or stream health events. There is no video.media.ready equivalent because Instagram handles all video processing internally. For apps that need to react to Instagram video content, these webhooks tell you what was posted but not how it was processed.
Every OTT platform gates premium content behind payment status. The question is how your backend knows a payment succeeded before the player requests a playback URL. Polling the payment gateway on every play request adds latency and burns API quota. Webhooks solve this by pre-resolving entitlement before the user ever hits play.
The pattern starts when a user initiates a payment. Your app redirects to the payment gateway. The user completes checkout. The gateway fires a webhook to your backend with a payment.success or payment.failed status.
Success path: The payment webhook arrives with status "completed." Your backend writes an entitlement row to the database: user X has access to content Y until date Z. When the player loads, it checks the entitlement table, finds a valid row, and requests the playback manifest from the CDN. Video plays. The player never waited for a payment check because the entitlement was already resolved.
Failure path: The payment webhook arrives with status "failed," or it never arrives at all (network issue, gateway timeout). Your backend keeps the entitlement locked. The player checks the table, finds no valid row, and shows a "Payment required" state. No playback URL is issued. No video streams.
The security angle matters here. Payment webhooks need the same signature verification as video webhooks. A spoofed payment.success POST to your endpoint grants unauthorized access to premium content. Use HMAC-SHA256 verification on payment webhooks exactly as described in the signature verification section above. Treat a payment webhook endpoint with the same paranoia as a login endpoint.
The fallback pattern is worth building too. If your webhook consumer goes down during a payment spike, users complete checkout but your backend never hears about it. Add a reconciliation check: on player load, if no entitlement row exists but the user claims they paid, do a single synchronous API call to the payment gateway as a fallback. This covers the edge case without adding polling to the happy path.
Our webhook schema is unified across every product: on-demand video, live streaming, in-video AI, video data, player, and cloud playout all emit events on the same video.* namespace. One subscription captures everything. One signature scheme verifies everything. One retry policy covers everything.
You create a webhook subscription with a POST to our webhooks endpoint, passing a URL and an optional list of event types to filter on. We sign every payload with HMAC-SHA256 and send the digest in a FastPix-Signature header computed over the raw body. Retries follow exponential backoff across a multi-hour window. Failed deliveries surface in the dashboard with full payload and response capture so you can inspect and resend.
Here is a minimal Express handler that verifies and routes events:
app.post('/webhooks/fastpix', express.raw({ type: 'application/json' }), async (req, res) => {
if (!verifyWebhook(req, process.env.FASTPIX_WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString());
const { type, data, id: eventId } = event;
switch (type) {
case 'video.media.ready':
await markAssetPlayable(data.id);
break;
case 'video.live_stream.created':
await storeStream(data.streamId, data.streamKey, data.playbackIds);
break;
case 'video.live_stream.active':
await notifyStreamLive(data.streamId);
break;
case 'video.live_stream.disconnected':
await endStreamSession(data.streamId);
break;
case 'video.media.failed':
await logEncodeFailure(data);
break;
}
res.status(200).send();
});
The payload shape matters here. Every FastPix webhook event includes a top-level type, an id (the webhook event ID for idempotency), a data object with the resource details, and an object field identifying the resource type and ID. For live streams, data.streamId and data.streamKey are the fields you need. For media assets, data.id gives you the asset identifier. Don't assume event.data.id works for both: stream events use streamId.
Yes. A video.media.failed event fires when encoding errors out after upload. Your webhook handler catches it, logs the failure reason from the payload, and updates the UI to show an error state instead of leaving the user staring at a spinner. Always subscribe to failure events alongside success events.
Webhooks fire on discrete state changes (upload complete, encode ready, stream active), not on continuous playback metrics like buffering. For real-time playback quality monitoring, including rebuffer ratio, startup time, and bitrate switches, you need a dedicated video data/analytics pipeline that collects player-side telemetry. Webhooks and QoE analytics solve different problems.
Design for failure. Use idempotency keys to prevent duplicate processing on retries. Build a reconciliation endpoint that queries the payment gateway on startup to catch events missed during downtime. Store every webhook event_id in a dedupe table. For payment-critical flows, add a fallback: if no webhook arrives within N seconds, do a single synchronous API check before blocking the user.
A webhook is server-to-server: your video API POSTs to your backend. SSE is server-to-client: your backend pushes events to the browser over a persistent HTTP connection. Use webhooks for backend orchestration (encoding done, payment verified, stream ended). Use SSE or WebSockets for real-time client updates (progress bars, live chat, viewer counts).
Yes, and this is one of the most common patterns. An upload completion webhook (video.media.created) triggers your backend to call the transcoding API. When transcoding finishes, a video.media.ready webhook fires. Your handler updates the database and makes the video playable. The entire chain runs on webhooks with zero polling.
One subscription with filtered event types is cleaner than multiple subscriptions pointing to the same URL. Subscribe to the events your app actually reacts to, typically 5-9 events covering upload, encoding, live streaming, and AI tracks. Every unused subscription adds retry load on your endpoint for zero value. Start narrow, expand when you add features that need new event types.
