Skip to content
Reference

Get started

Overview

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"
}
EventResponseEffect
room_started200 {"ok": true, "event": "room_started"}upsert, set started_at
room_finished200 {"ok": true, "event": "room_finished"}upsert, set ended_at, duration_s, participant_count
anything else200 {"ok": true, "ignored": "<kind>"}none
no room.name200 {"ok": true, "ignored": "<kind>"}none
bad signature401none
body is not JSON400none
handler threw500retry 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.

Up nextControl plane API