08 โ Receiving webhooks
Get real-time server-side notifications when things happen in your calls โ rooms start/finish, participants join/leave, recordings finish. arzbridge POSTs a signed JSON event to an endpoint you register, so your backend can react (update your DB, notify users, trigger workflows) without polling.
Webhooks are server-to-server. Register an endpoint on your backend, not in a mobile/web app.
1. Register an endpoint
Via the API (needs the webhooks.write scope) โ the signing secret is returned
once, store it:
curl -s https://calls.arzbridge.com/api/v1/webhook-endpoints \
-H "Authorization: Bearer $KEY:$SECRET" -H "Content-Type: application/json" \
-d '{
"url": "https://api.yourapp.com/webhooks/calls",
"description": "production",
"events": ["participant_joined","participant_left","room_finished"]
}'
# -> { "data": { "id": 7, "secret": "whsec_โฆ", "active": true, ... } }
Omit events (or pass []) to receive all event types. Manage endpoints with
GET/DELETE /api/v1/webhook-endpoints and POST /api/v1/webhook-endpoints/{id}/test.
(Or ask arzbridge to add it for you.)
Your URL must be public HTTPS โ private/loopback addresses are rejected.
2. The request we send
POST to your URL, Content-Type: application/json, with headers:
| Header | Example | Meaning |
|---|---|---|
X-Arzbridge-Event |
participant_joined |
The event type |
X-Arzbridge-Delivery |
4821 |
Unique delivery id (for your logs) |
X-Arzbridge-Signature |
t=1751280000,v1=9f86d0โฆ |
HMAC signature (verify this!) |
Body (room names are your names, never the internal ones):
{
"id": "EV_8s2โฆ",
"event": "participant_joined",
"created_at": 1751280000,
"room": "support-42",
"participant": { "identity": "user-1", "name": "Alice", "sid": "PA_โฆ" },
"egress": {
"egress_id": "EG_โฆ",
"status": 3,
"files": [
{ "filename": "โฆmp4", "location": "โฆ", "size": 1048576, "duration": 42000000000 }
]
}
}
participant is present on participant/track events; egress on recording events.
On egress_ended (recording finished, status: 3) the egress.files[] array
lists the produced file(s) โ filename, location, size (bytes) and duration
(nanoseconds). (files is omitted until the recording completes.)
3. Verify the signature (always do this)
The signature is HMAC-SHA256( "<t>.<raw-body>", secret ), hex-encoded.
Use the raw request body (not a re-serialized object) and compare in constant
time. Optionally reject timestamps older than ~5 min to prevent replay.
Node.js (Express)
const crypto = require('crypto');
const SECRET = process.env.ARZ_WEBHOOK_SECRET;
// IMPORTANT: capture the RAW body for this route
app.post('/webhooks/calls', express.raw({ type: '*/*' }), (req, res) => {
const m = (req.header('X-Arzbridge-Signature') || '').match(/t=(\d+),v1=([a-f0-9]+)/);
if (!m) return res.sendStatus(400);
const [, t, v1] = m;
const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${req.body}`).digest('hex');
const ok = expected.length === v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
if (!ok) return res.sendStatus(401);
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(401); // replay guard
const event = JSON.parse(req.body.toString());
// ...handle event.event, event.room, event.participant...
res.sendStatus(200); // ACK fast (within ~10s)
});
Laravel
// routes/api.php (api routes are CSRF-exempt)
use Illuminate\Http\Request;
Route::post('/webhooks/calls', function (Request $r) {
$sig = $r->header('X-Arzbridge-Signature', '');
if (! preg_match('/t=(\d+),v1=([a-f0-9]+)/', $sig, $m)) abort(400);
[, $t, $v1] = $m;
$expected = hash_hmac('sha256', $t.'.'.$r->getContent(), config('services.arzcalls.webhook_secret'));
abort_unless(hash_equals($expected, $v1), 401);
abort_if(abs(time() - (int) $t) > 300, 401); // replay guard
$event = $r->json()->all();
// ...handle $event['event'], $event['room'], $event['participant']...
return response()->noContent(); // 200/204
})->name('calls.webhook');
4. Event types
room_started, room_finished, participant_joined, participant_left,
track_published, track_unpublished, egress_started, egress_updated,
egress_ended. Subscribe to a subset via events, or receive all.
5. Delivery, retries & best practices
- Respond
2xxquickly (within ~10s). Do heavy work asynchronously โ just acknowledge first. - Retries: non-2xx / timeouts are retried up to 5 times with backoff (~10s, 30s, 2m, 5m, 15m). An endpoint that keeps failing is auto-disabled.
- Idempotency: the same delivery may arrive more than once (e.g. after a
retry). De-duplicate on the event
id. - Ordering is not guaranteed; use
created_atif you need sequence. - Keep the secret safe; rotate by deleting and re-creating the endpoint.
- Test anytime with
POST /api/v1/webhook-endpoints/{id}/test(sends aping).
That's it โ your backend now stays in sync with every call in real time.