Gareth Wilson Gareth Wilson

Guide to api.video Webhooks: Features and Best Practices

Published


api.video is an API for uploading, encoding, hosting and live streaming video. Most of its work is asynchronous: an upload has to be transcoded into several renditions, a live stream has to start, end and be recorded, and captions and summaries are generated by AI after the fact. Webhooks are how your application finds out when each of those steps finishes, so it can switch on playback, update a player or publish a transcript without polling the API.

This guide covers api.video's six events, why one upload produces several encoding webhooks, how to verify X-Api-Video-Signature when you have more than one subscription, and what api.video's very short retry window means for your receiver.

What are api.video webhooks?

A webhook in api.video is a subscription: a URL and a list of events. When one of those events happens in the project, api.video sends a POST with a JSON body to the URL. You can create subscriptions in the dashboard or through the API, list and delete them, and look up any one by its ID. There is no update call, so changing the events means deleting the subscription and creating a new one.

Each subscription gets its own signature secret, prefixed sig_sec_. Every delivery carries two headers: X-Api-Video-WebhookID, which names the subscription that sent it, and X-Api-Video-Signature, a hex HMAC-SHA256 of the body keyed with that subscription's secret.

api.video webhook features

FeatureDetails
ConfigurationDashboard (Project settings > Webhooks) or POST https://ws.api.video/webhooks
ManagementList, retrieve and delete. No update endpoint
Signaturehex HMAC-SHA256
Signature headerx-api-video-signature
Subscription headerX-Api-Video-WebhookID
Signed contentThe raw request body only
SecretOne signatureSecret (sig_sec_...) per subscription
BodyJSON
Event routingBody field type
SuccessAny 1xx or 2xx response
Retries3 retries at 3-second intervals for any response above 299
TimeoutNot documented
Source IPsNot published

Events and payloads

EventTriggered WhenCommon Use Cases
video.source.recordedThe video's source file has been recordedConfirm an upload landed
video.encoding.quality.completedA rendition finished encodingEnable playback at that quality
video.caption.generatedA caption was generatedPublish captions
video.summary.generatedA summary was generatedStore the summary
live-stream.broadcast.startedA broadcast startedNotify viewers, mark live
live-stream.broadcast.endedA broadcast endedFinalize the recording

api.video's own description of video.source.recorded is narrower than its name suggests: it fires when "a live stream has been completed and the recording of the live stream is ready to be transcoded". It is the bridge between the live stream events and the encoding events for the recording.

video.encoding.quality.completed is the event you will receive most. The MP4 asset is transcoded once, so it sends one webhook at the final source quality. The HLS asset sends one webhook for each quality from 240p up to the source quality, up to 4K. api.video's example is a 720p upload, which produces five webhooks: one for MP4 at 720p and four for HLS at 240p, 360p, 480p and 720p. Use encoding and quality to decide when a video is ready for your purposes, rather than treating the first webhook as "done".

Event Payload Structure

Bodies are flat. type and emittedAt sit beside the event's own fields, such as videoId, liveStreamId, encoding and quality. The X-Api-Video-WebhookID header names the subscription, not the event.

Published payload example

This is the video.encoding.quality.completed sample from the webhook samples library. It is api.video's documentation example, not a captured delivery.

{
  "type": "video.encoding.quality.completed",
  "emittedAt": "2021-01-29T15:46:25.217Z",
  "videoId": "vi0000000000000000000000",
  "liveStreamId": "li0000000000000000000000",
  "encoding": "hls",
  "quality": "720p"
}

Only this event has an example on api.video's webhook guide. The API reference describes the others, so log a real delivery of each event you handle before relying on its fields.

Setting up api.video webhooks

In the dashboard, open Project settings, then the Webhooks tab, and add your URL with the events you want. The same tab lists your subscriptions, filters them by event and deletes them.

To create a subscription from code, call the API with your API key:

curl --request POST \
  --url https://ws.api.video/webhooks \
  --header 'Authorization: Bearer {your API key}' \
  --header 'Content-Type: application/json' \
  --data '{"events": ["video.encoding.quality.completed", "live-stream.broadcast.started"], "url": "https://example.com/webhooks/api-video"}'

The 201 response includes webhookId and signatureSecret. Store the secret server-side, for example as WEBHOOK_SECRET. You can read it again later with GET /webhooks/{webhookId}, using the ID from the X-Api-Video-WebhookID header if you need to find out which subscription sent a request. api.video's sandbox environment (sandbox.api.video) has its own keys and its own webhooks, so test there before you subscribe in production.

To receive sandbox deliveries on your machine, forward them with the Hookdeck CLI:

npx hookdeck-cli listen 3000 api-video --path /webhooks/api-video

Securing api.video webhooks

Read X-Api-Video-Signature, compute an HMAC-SHA256 of the raw request body with the subscription's secret as the key, encode it as hex and compare the two in constant time. api.video warns that the body must be byte-identical to what it sent, so don't parse it first. See the verification documentation.

const crypto = require('node:crypto');

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['x-api-video-signature'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const message = rawBody;
  const candidates = [signature];
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

If you run more than one subscription, each has a different secret. The handler below picks the secret by the X-Api-Video-WebhookID header, with WEBHOOK_SECRET as the fallback for a single subscription, and stores the event before answering.

const express = require('express');

// e.g. { "webhook_1BWt3JArmQq9sNZzsHStRS": "sig_sec_..." }
const secretsByWebhookId = JSON.parse(process.env.API_VIDEO_WEBHOOK_SECRETS || '{}');

function createApiVideoApp(saveEvent) {
  const app = express();
  app.post('/webhooks/api-video', express.raw({ type: '*/*' }), async (req, res) => {
    const subscriptionId = req.get('x-api-video-webhookid');
    const secret = secretsByWebhookId[subscriptionId] || process.env.WEBHOOK_SECRET;
    if (!secret) return res.sendStatus(500);
    if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

    let event;
    try {
      event = JSON.parse(req.body.toString('utf8'));
    } catch {
      return res.sendStatus(400);
    }
    if (!event || typeof event !== 'object' || Array.isArray(event)) return res.sendStatus(400);
    if (typeof event.type !== 'string') return res.sendStatus(400);
    try {
      if (event.type === 'video.encoding.quality.completed') {
        await saveEvent('rendition', { videoId: event.videoId, encoding: event.encoding, quality: event.quality });
      } else {
        await saveEvent(event.type, event);
      }
    } catch {
      return res.sendStatus(503);
    }
    return res.sendStatus(200);
  });
  return app;
}

Only the body is signed. emittedAt is inside the body, and api.video sends no separate signed timestamp, so a captured request verifies for as long as the secret is valid.

Hookdeck's api.video source type verifies the same header, a hex HMAC-SHA256 of the raw body, using the secret you paste into the source. Hookdeck always answers api.video with 200, records a mismatch as failed verification and does not forward that request. There is no timestamp check on Hookdeck's side either. Because the secret is per subscription, use one Hookdeck source per api.video subscription.

Make api.video webhooks production-ready. Hookdeck Event Gateway accepts every api.video delivery straight away, verifies X-Api-Video-Signature, and keeps each encoding and live stream event until your service has processed it.

api.video webhook limitations and pain points

All retries happen within about ten seconds

The Problem: A short restart or deploy on your side loses encoding and broadcast events for good.

Why It Happens: api.video retries an unsuccessful delivery 3 times with 3-second intervals between tries. Any status above 299 counts as unsuccessful. After the last try, there is no further attempt and no documented way to redeliver.

Workarounds:

  • Keep the receiver highly available, and return 2xx as soon as the event is stored.
  • Reconcile periodically: list videos with the API and check their status, so a missed video.encoding.quality.completed doesn't leave a video unpublished.

How Hookdeck Can Help: Hookdeck accepts each delivery on api.video's first attempt and stores it, then retries your service on a schedule you choose, so your downtime costs latency instead of events.

Many encoding webhooks for one upload

The Problem: Your handler marks a video as ready after the first webhook, and viewers get 240p.

Why It Happens: HLS sends one video.encoding.quality.completed per rendition, from the lowest quality up, and MP4 sends its own. A single 720p upload produces five webhooks.

Workarounds:

  • Track renditions per videoId and act when the quality you need has arrived.
  • Use the encoding field to tell HLS and MP4 apart.

How Hookdeck Can Help: Hookdeck filters can match on encoding and quality, so only the renditions you act on reach your service.

A secret per subscription

The Problem: Verification fails for some deliveries and passes for others.

Why It Happens: Every subscription has its own signatureSecret. Two subscriptions pointing at the same URL sign with different keys, and a recreated subscription gets a new one.

Workarounds:

  • Map secrets by X-Api-Video-WebhookID, as in the handler above.
  • When you delete and recreate a subscription to change its events, update the stored secret at the same time.

How Hookdeck Can Help: Give each subscription its own Hookdeck source with its secret, and route them all to one destination.

No timeout, IP list or event IDs

The Problem: You can't firewall the endpoint, tune your response time to a known limit, or deduplicate on an event ID.

Why It Happens: api.video's webhook documentation states no timeout, publishes no source IPs, and puts no event ID in the body.

Workarounds:

  • Treat the signature as the only proof of origin.
  • Answer quickly, without waiting on downstream work.
  • Build an idempotency key from type, the resource ID, encoding and quality.

How Hookdeck Can Help: Hookdeck's deduplication can use a combination of body fields as the key, so retried deliveries of the same rendition are dropped before your service sees them.

Best practices

Verify the exact bytes

The signature covers the body as sent. In Express, use express.raw({ type: '*/*' }). In Next.js, use await request.arrayBuffer(). In FastAPI, use await request.body(). Parse JSON only after verification.

Decide what "ready" means

Pick the rendition your product needs, for example HLS at 720p or the MP4, and only flip a video to published when that quality arrives. Keep a record of renditions received so late or retried webhooks don't move the state backwards.

Close the loop on live streams

Use live-stream.broadcast.started and live-stream.broadcast.ended to mark a stream as live, and video.source.recorded to know a recording is on its way. The recording's own encoding webhooks follow.

Acknowledge fast and process asynchronously

With retries at 3-second intervals, a slow handler can burn through api.video's attempts before it ever answers. Verify, store and return 200, then do the work. See why to process webhooks asynchronously.

Make processing idempotent

Retries resend the same body. Key work on videoId plus encoding and quality, or on liveStreamId plus type. More patterns are in our webhook idempotency guide.

Conclusion

api.video's webhooks are simple to verify and easy to subscribe to, but its delivery is unforgiving: three retries a few seconds apart, then nothing. Treat the encoding webhooks as a stream of renditions rather than a single "done" signal, map secrets by subscription ID, and keep the endpoint answering fast.

Hookdeck Event Gateway gives api.video an endpoint that is always up, verifies each signature and holds the event until your service is ready for it.

Get started with Hookdeck for free and handle api.video webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.