Webhook and ingest API
The two endpoints other machines post to, and how each one authenticates.
Both write to the same call row and authenticate completely differently, because one is a third party signing over a body and the other is your own worker carrying a secret you issued.
Implemented in server/lg_webhooks.py.
POST /public/livegrid/webhook#
The media server, reporting lifecycle. Public by necessity — it has no customer cookie — so the signature is the only gate.
Authentication. An Authorization header carrying a JWT signed with
LIVEKIT_API_SECRET, whose sha256 claim is the base64 digest of the raw
request body. Both halves are checked: a valid signature over a different body
is the whole attack.
With no secret configured, every request is refused. A receiver that silently degrades to trusting anybody is worse than one that is switched off.
{
"event": "room_started",
"room": { "name": "support-8f3a21", "sid": "RM_...", "creationTime": 1789... },
"region": "sg"
}
| Event | Response | Effect |
|---|---|---|
room_started | 200 {"ok": true, "event": "room_started"} | upsert, set started_at |
room_finished | 200 {"ok": true, "event": "room_finished"} | upsert, set ended_at, duration_s, participant_count |
| anything else | 200 {"ok": true, "ignored": "<kind>"} | none |
no room.name | 200 {"ok": true, "ignored": "<kind>"} | none |
| bad signature | 401 | none |
| body is not JSON | 400 | none |
| handler threw | 500 | retry me |
The 500 is deliberate. Both handlers upsert on (room_name, region), so a
redelivery converges on the same row rather than duplicating the call — which is
what makes asking for a retry safe. Events with no room are acknowledged rather
than rejected, because the media server retries a non-2xx and retrying an event
that will never be stored is a loop with no exit.
region comes from the event, then LIVEGRID_REGION, then NULL — handled by
NULLS NOT DISTINCT on the unique constraint.
POST /public/livegrid/ingest#
Your own agent worker, handing over one finished call's detail.
Authentication. Authorization: Bearer $LIVEGRID_INGEST_SECRET — a shared
secret you issue, compared in constant time. Unset, the endpoint answers 503
{"error": "ingest is not configured"} rather than accepting anything.
{
"room_name": "support-8f3a21",
"region": "sg",
"outcome": "completed",
"agent_name": "livegrid",
"llm_model": "...",
"events": [ { "seq": 1, "role": "user", "text": "...", "latency_ms": null } ],
"tools": [ { "tool_name": "lookup_order", "arguments_json": "{...}",
"result_summary": "...", "success": true } ]
}
Everything here is best-effort by design. The worker has already hung up by the time it posts; a rejection loses a transcript and nothing else.
Same destination row, reached by the same upsert, so order does not matter —
the worker usually finishes before room_finished arrives, and nothing depends
on it.
GET /public/livegrid/webhook/health#
Whether the receiver is configured — that is, whether a signing secret is present. It does not tell you the media server is actually sending anything. If rows have stopped appearing and this says configured, the problem is upstream.