Webhooks are global only: no way to route a single room's events to its own endpoint
Problem
Webhook delivery is configured once, globally:
webhook:
api_key: <api_key>
urls:
- https://your-host.com/handlerEvery room's events go to those URLs. There is no way to say "this room's events go to this endpoint".
That is awkward for anything multi-tenant. If one deployment hosts rooms for several customers, teams, or environments, every event lands on one handler that then has to fan back out — and the routing key has to be smuggled through room metadata or reconstructed from the room name. The subscriber also cannot be given a per-room signing key, so one shared secret covers every tenant's traffic.
Why this is odd today
Egress already solves exactly this problem for its own resources. StartEgressRequest, RoomCompositeEgressRequest, TrackEgressRequest and friends all carry:
repeated WebhookConfig webhooks = N;The request is kept in the resource state (EgressInfo.Request), and egress.GetEgressNotifyOptions turns it into a webhook.WithExtraWebhooks option at emit time.
The delivery machinery for this is already built and already generic: webhook.DefaultNotifier.QueueNotify sends to the static config URLs first, then to each extra webhook, resolving SigningKey through the KeyProvider and honouring per-webhook FilterParams. Rooms simply never supply the option.
Proposal
Give rooms the same treatment:
CreateRoomRequest.webhooks— attach webhooks to a single room at creation.RoomConfiguration.webhooks— attach them to a named room preset inconfig.yaml, selected withroom_preset.RoomInternal.webhooks— persist the resolved list. This is the analogue ofEgressInfo.Request. Deliberately not on the publicRoomproto, since entries hold signing keys andRoomis returned byListRooms.webhook.GetRoomNotifyOptions— mirror ofegress.GetEgressNotifyOptions.
Usage would be:
// CreateRoom
{
"name": "support-call",
"webhooks": [
{ "url": "https://tenant-a.example.com/hook", "signing_key": "APIxxx" }
]
}Scope
Room webhooks should receive room, participant and track events only:
room_started, room_finished, participant_joined, participant_left, participant_connection_aborted, track_published, track_unpublished
Egress and ingress events should be excluded by an allowlist. Those resources already carry their own webhook config on the request that created them, so forwarding them to a room webhook would both duplicate delivery and leak beyond the room's scope.
The change is additive — behaviour is unchanged when no room webhooks are configured.
PRs
I have this implemented and tested across the two repos:
- livekit/protocol#1777 — the three proto fields and the
webhook.GetRoomNotifyOptionshelper, plus the event allowlist and its tests. - livekit/livekit#4848 — the server side: resolution in
roomallocator.go, threading through the six webhook-emitting telemetry methods,CreateRoomvalidation of URLs and signing keys, aconfig-sample.yamlnote, unit tests, and an end-to-end test asserting a room's own endpoint receives its events while the global endpoint still receives everything.
The server PR is a draft until the protocol one lands, since it needs the proto fields.
Happy to adjust the approach — in particular, threading the webhook list through the telemetry methods versus keeping a per-room registry inside telemetryService is a judgement call I'd like a maintainer's opinion on. I went with threading because RoomIDChanged re-keys a room mid-session and a keyed registry would then silently stop matching.
Source: livekit/livekit