Guide to 100ms Webhooks: Features and Best Practices
100ms webhooks report what happens inside your live video and audio rooms. Every session produces a stream of events, such as peers joining and leaving, sessions opening and closing, recordings finishing and HLS streams starting, and 100ms posts each of them to your server. That's how you build attendance records, bill for usage, pick up finished recordings or react when a stream fails, without polling the 100ms API.
This guide covers how 100ms webhooks are configured, the events and payload format, how to authenticate requests with secret headers and the IP allowlist, how 100ms retries, and the practices that keep a 100ms receiver from losing events.
What are 100ms webhooks?
Each 100ms workspace has its own webhook configuration, set in the Developer section of the 100ms dashboard. 100ms sends a JSON POST to that URL for every event in the workspace, and your endpoint must accept POST and answer 200 OK.
100ms doesn't sign webhooks. Its documentation offers two ways to secure them:
- Secret headers. You set headers in the webhook configuration, and 100ms passes them "transparently to your webhook endpoint". Your endpoint checks the header name and value.
- IP allowlisting. 100ms publishes the addresses of its own NAT gateways, so a firewall can allow only those.
100ms webhook features
| Feature | Details |
|---|---|
| Configuration | One webhook per workspace, in the dashboard's Developer section |
| Method | POST with a JSON body |
| Event field | type, in the form object.action.outcome |
| Envelope | version, id, app_id, account_id, template_id, timestamp, type, data |
| Authentication | Secret headers you define. No signature or timestamp check |
| Source IPs | 61 published addresses of 100ms-owned NAT gateways |
| Success | HTTP 200 |
| Retries | Three, at 1 second, 3 seconds and 10 seconds after the previous attempt |
| Timeout | Not documented |
Common events
| Area | type | Fires when |
|---|---|---|
| Sessions | session.open.success, session.close.success | A session starts or ends |
| Peers | peer.join.success, peer.leave.success, peer.join.failure, peer.leave.failure | A peer joins or leaves a room, or fails to |
| Rooms and roles | room.end.success, room.end.failure, role.change.success, role.change.failure | A room is ended, or a peer's role changes |
| Recordings | track.recording.started.success, track.recording.success, track.recording.failure, stream.recording.success, stream.recording.failure, recording.success, recording.failed | Track, stream or composed recordings start, finish or fail |
| RTMP streaming and browser recording | beam.started.success, beam.stopped.success, beam.recording.success, beam.failure | Streaming or browser recording starts, stops, uploads or fails |
| HLS | hls.started.success, hls.stopped.success, hls.recording.success, hls.failure | HLS streaming or recording starts, stops, uploads or fails |
| Transcription | transcription.started.success, transcription.success, transcription.failure | Post-recording transcription starts, succeeds or fails |
| RTMP ingest | ingest.start.success, ingest.end.success, ingest.start.failure, ingest.failure | Incoming RTMP starts, ends or fails |
| SIP | sip.start.success, sip.start.failure, sip.end.success, sip.end.failure | A SIP call is answered, fails to connect, or ends |
Notice that the composed recording failure is recording.failed, not recording.failure. 100ms's summary table also leaves out room.end.failure and role.change.failure, which have their own sections further down the page.
Every event uses the same envelope, with the event's own fields in data. Here is 100ms's sample event:
{
"version": "2.0",
"id": "6c75de35-b778-4fb0-88ae-114c28dc285f",
"account_id": "60b8e13a6eb86d8101b57354",
"timestamp": "2021-08-08T07:04:15Z",
"type": "peer.join.success",
"data": {
"joined_at": "2021-08-08T07:04:15.001380432Z",
"peer_id": "83b869e1-9a4b-4037-84b2-913cf76e4392",
"role": "host",
"room_id": "60b8e13a6eb86d8101b57354",
"room_name": "test room",
"session_id": "610f81ee870dde099a249948",
"template_id": "66112497abcd52312556c4gg",
"user_id": "user.001",
"user_name": "test user"
}
}
The envelope table also lists app_id and template_id at the top level, which this sample omits, so treat them as optional. The per-event samples in the docs have their IDs masked with asterisks, and the track.recording.failure sample shows "type": "track.recording.success", so trust the section headings over the sample values.
Setting up 100ms webhooks
- Pick a header name, such as
X-100ms-Secret, and generate a long random value withopenssl rand -hex 32. - In the 100ms dashboard, open the Developer section and find your workspace's webhook configuration.
- Enter your HTTPS endpoint URL.
- Add the header name and value as a secret header.
- Save, then start a test room to generate events.
If your firewall can block inbound traffic, allow the IP addresses listed on 100ms's Secure Your Webhooks page as well. Keep the header check in place: the IP list is a second layer, not a replacement.
With Hookdeck. 100ms webhooks are not signed. Add a secret header to the webhook in the 100ms dashboard, then create a Hookdeck source with the 100ms type and enter the same name in API Key Header and the exact value in API Key. Hookdeck finds the header case-insensitively and compares the whole value, so for Authorization: Bearer abc123 enter Bearer abc123. Use the source URL as the webhook URL in 100ms. The source accepts POST only, which is what 100ms sends.
Securing 100ms webhooks
Read the header you configured, compare its whole value with the stored one in constant time, then parse the body.
const crypto = require('crypto');
const express = require('express');
const HEADER = (process.env.HUNDRED_MS_WEBHOOK_HEADER || 'X-100ms-Secret').toLowerCase();
const SECRET = process.env.HUNDRED_MS_WEBHOOK_SECRET; // the exact, whole header value
function safeEqual(received, expected) {
const a = Buffer.from(received, 'utf8');
const b = Buffer.from(expected, 'utf8');
// timingSafeEqual throws on unequal lengths, so check first.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
app.post('/webhooks/100ms', express.raw({ type: '*/*' }), (req, res) => {
const received = req.get(HEADER);
if (!SECRET || typeof received !== 'string' || !safeEqual(received, SECRET)) {
return res.sendStatus(401);
}
let event;
try {
event = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
// Deduplicate on event.id, queue by event.type, then answer with exactly 200.
res.sendStatus(200);
});
The same check in Python:
import hmac
import os
HEADER = os.environ.get("HUNDRED_MS_WEBHOOK_HEADER", "X-100ms-Secret")
SECRET = os.environ.get("HUNDRED_MS_WEBHOOK_SECRET", "")
def verify_100ms_request(headers) -> bool:
if not SECRET:
return False # fail closed when unconfigured
received = headers.get(HEADER) # Starlette and Flask headers are case-insensitive
if not received:
return False
# Compare bytes: compare_digest raises TypeError on a non-ASCII str.
return hmac.compare_digest(received.encode("utf-8"), SECRET.encode("utf-8"))
A static header authenticates the sender, not the body. It can't detect a modified payload, and with no signed timestamp a captured request can be replayed and still pass. Use HTTPS, a long random value, and the IP allowlist where your infrastructure supports it.
Make 100ms webhooks production-ready. Hookdeck Event Gateway checks your secret header on every 100ms event, answers with a
200straight away, and delivers each event to your service with retries you control.
100ms webhook limitations and pain points
Three retries in 14 seconds
The Problem: A deploy, a restart or a short outage loses events for good.
Why It Happens: 100ms retries a failed webhook three times: one second after the original attempt, three seconds after the first retry and ten seconds after the second. That is about 14 seconds in total, and 100ms documents no further attempts.
Workarounds:
- Keep the receiver highly available, and answer before doing any slow work.
- Reconcile important state, such as recordings, against the 100ms API rather than relying on every webhook arriving.
How Hookdeck Can Help: Hookdeck answers 100ms with a 200 as soon as the event is stored, then delivers it to your service with retries over as long a window as you configure. Events that arrive while your service is down wait in Hookdeck instead of disappearing.
Only 200 counts
The Problem: Your endpoint returns 202 Accepted or 204 No Content, and you receive the event again.
Why It Happens: 100ms says it retries "if your server does not return an HTTP 200 status code". It doesn't say other 2xx codes count as success.
Workarounds:
- Return exactly
200. - Deduplicate on the event
idin case a retry gets through anyway.
How Hookdeck Can Help: Hookdeck answers 100ms with a 200, whatever your own service returns.
High event volume from busy rooms
The Problem: A large session floods your endpoint with peer events, and slow responses turn into retries.
Why It Happens: Every peer join and leave is its own webhook, and they all go to the workspace's single URL.
Workarounds:
- Queue events and process them asynchronously.
- Ignore event types you don't use rather than doing work for each.
How Hookdeck Can Help: Hookdeck absorbs the burst and delivers to your service at a max delivery rate you set. Filters can drop the event types you don't need before they reach you.
One header for the whole workspace
The Problem: A wrong header value after a rotation means every event in the workspace fails.
Why It Happens: There is one webhook configuration per workspace, and the secret header is a static value.
Workarounds:
- Rotate by accepting both values in your receiver, updating the dashboard, then removing the old value.
How Hookdeck Can Help: Hookdeck's 100ms source compares the header you name with the exact value you enter. Hookdeck answers 100ms with a 200 either way, then records a request without the right header as failed verification, so it never reaches your destination, and you can see which headers arrived.
Best practices
Route on type
Switch on type and acknowledge anything you don't recognise, so an event type you haven't seen yet doesn't fail and get retried.
Deduplicate on id
Each event has an id. Store it before processing and skip repeats. See our guide to webhook idempotency.
Order by timestamp, not arrival
Peer and session events from the same room can arrive close together. Use timestamp and the fields in data, such as joined_at, to order them.
Acknowledge fast
With about 14 seconds of retries, speed matters more than usual. Verify, store, return 200, and process afterwards. See why to process webhooks asynchronously.
Conclusion
100ms sends one JSON POST per room, session, recording or streaming event, with the event name in type and no signature. Add a secret header and compare its whole value, allow 100ms's IP addresses where you can, deduplicate on id, and answer with exactly 200 quickly, because three retries over about 14 seconds are all you get.
Hookdeck Event Gateway sits in front of your receiver, checks the header, answers 100ms at once, and holds every event until your service has processed it.
Get started with Hookdeck for free and handle 100ms webhooks reliably in minutes.