Webhooks and events
The media server reports the lifecycle. Verifying the signature is the whole gate.
The media server tells the API that a room opened or closed. Those two events are the authoritative record of a call's lifecycle.
POST /public/livegrid/webhook
Why the media server and not the worker#
The worker sees far more of a call and is not sufficient on its own — it only runs when an agent was dispatched, and a worker that crashes takes its unwritten record with it, losing exactly the calls most worth looking at.
The media server has neither problem: it is the thing that opens and closes the room. So lifecycle comes from here and is authoritative; content comes from the worker and is best-effort. Transcripts
Signature verification is the entire gate#
This endpoint is public by necessity — the media server has no customer cookie — so the signature is the only thing between an open room-writing API and the internet.
LiveKit signs with the same key pair used to mint tokens: an Authorization
header carrying a JWT whose sha256 claim is the base64 digest of the raw
request body. Verifying it means checking the JWT and hashing the body
yourself. A valid signature over a different body is the whole attack, and it is
what you get if you trust the header alone.
With no secret configured the endpoint refuses every request rather than accepting unsigned ones. A webhook receiver that silently degrades to trusting anybody is worse than one that is switched off, because it looks like it is working.
What it does with each event#
| Event | Effect |
|---|---|
room_started | upsert a call row, set started_at |
room_finished | upsert the same row, set ended_at, duration_s, participant_count |
| anything else | 200 {"ok": true, "ignored": "<kind>"} |
Events about tracks and egress carry no room to key on, so they are acknowledged rather than rejected — the media server retries on a non-2xx, and retrying an event that will never be stored is a loop with no exit.
A handler that throws returns 500 on purpose, so the media server retries.
Both handlers upsert on (room_name, region), so a redelivery converges on the
same row instead of duplicating the call — which is what makes asking for a
retry safe.
Region#
region comes from the event, then from LIVEGRID_REGION, then NULL. A
single-region deployment leaves it NULL and the unique constraint handles that
with NULLS NOT DISTINCT.
Tenancy#
The media server knows nothing about organisations — it routes packets. Until a
room name carries an org, calls are attributed to LIVEGRID_DEFAULT_ORG_ID.
Left unset, calls are still recorded with org_id NULL rather than dropped. An
unattributed record is recoverable; a discarded one is not.
Full payloads on Webhook and ingest API.