Agent skill
100ms Webhooks Skill
Receive and authenticate 100ms webhooks. Use when setting up a 100ms (live video) webhook endpoint, checking the secret header you configure in the 100ms dashboard, debugging 401s, or handling events like peer.join.success, peer.leave.success, session.open.success, session.close.success, recording.success and hls.started.success.
Install this skill
npx skills add hookdeck/webhook-skills --skill 100ms-webhooks
100ms (live video SDK) sends a JSON POST to your endpoint when something happens in a room: a peer joins or leaves, a session opens or closes, a recording or HLS stream starts or finishes.
100ms webhooks are not signed. Add a secret header to the webhook in the 100ms dashboard and enter it here. Your endpoint authenticates each request by comparing that header with the value you stored.
When to Use This Skill
- How do I receive 100ms webhooks?
- Does 100ms sign webhook payloads? (No: use a secret header)
- How do I check the secret header on a 100ms webhook?
- How do I handle
peer.join.success,session.close.successorrecording.successevents? - Why does my 100ms webhook endpoint return 401?
Verification (core)
const crypto = require('crypto');
// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(received, expected) {
const a = crypto.createHash('sha256').update(Buffer.from(received, 'latin1')).digest();
const b = crypto.createHash('sha256').update(Buffer.from(expected, 'utf8')).digest();
return crypto.timingSafeEqual(a, b);
}
function verify100msRequest(headers, secret = process.env.HUNDRED_MS_WEBHOOK_SECRET) {
if (!secret) return false; // never accept when unconfigured
const name = (process.env.HUNDRED_MS_WEBHOOK_HEADER || 'Authorization').toLowerCase();
const received = headers[name]; // Node lowercases header names
return typeof received === 'string' && received !== '' && safeEqual(received, secret);
}
- The header name is whatever you chose in the dashboard (
HUNDRED_, defaultMS_ WEBHOOK_ HEADER Authorization), looked up case-insensitively. - Compare the whole value (
HUNDRED_), including anyMS_ WEBHOOK_ SECRET Bearerprefix you typed. Do not strip or parse the prefix. - Python:
hmac.compare_digeston bytes (encode both sides; never passstr, which raises on non-ASCII). - Return
401for a missing header or mismatch,400for a body that isn't a JSON object with atype, and200once handled. 100ms retries three times on any other status. - Read the body raw (
express.raw({ type: '*/*' })) and authenticate before parsing it. - A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS and rotate the credential if it leaks. Optionally also allowlist the 100ms IP addresses (listed in their docs).
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Types
The body is {version, id, app_. type is <object>.<action>.<outcome>; dispatch on it and dedupe on id. The webhook must accept POST.
| Event | Sent When |
|---|---|
session.open.success, session.close.success | A session starts or ends |
peer.join.success, peer.leave.success | A peer joins or leaves a room |
peer.join.failure, peer.leave.failure, room.end.failure, role.change.failure | A peer join or leave, room end or role change fails |
room.end.success | A room ends |
role.change.success | A peer's role is updated |
recording.success, recording.failed | An SFU recording ends |
track.recording.started.success, track.recording.success, track.recording.failure | A track recording starts, ends or fails |
stream.recording.success, stream.recording.failure | A stream recording ends or fails |
beam.started.success, beam.stopped.success, beam.recording.success, beam.failure | RTMP streaming or browser recording starts, stops, ends or fails |
hls.started.success, hls.stopped.success, hls.recording.success, hls.failure | HLS streaming or recording starts, stops, ends or fails |
transcription.started.success, transcription.success, transcription.failure | Transcription starts, ends or fails |
ingest.start.success, ingest.end.success, ingest.start.failure, ingest.failure | RTMP ingestion starts, ends or fails |
sip.start.success, sip.start.failure, sip.end.success, sip.end.failure | An inbound or outbound SIP call connects, fails or ends |
Environment Variables
HUNDRED_MS_WEBHOOK_HEADER=Authorization # header name you chose (default: Authorization)
HUNDRED_MS_WEBHOOK_SECRET=your-secret-value # exact, whole header value
The prefix is HUNDRED_MS because an environment variable name can't start with a digit.
Local Development
100ms can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 100ms --path /webhooks/100ms
Use the printed URL as the webhook URL in the 100ms dashboard.
Resources
- references/overview.md - What 100ms webhooks are, events and payload shape
- references/setup.md - Configure the webhook and secret header in the dashboard
- references/verification.md - Secret header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests