#4849·livekit

Webhooks are global only: no way to route a single room's events to its own endpoint

Author: Darshak03Created Sep 7, 2026Updated Sep 8, 2026

Problem

Webhook delivery is configured once, globally:

yaml
webhook:
  api_key: <api_key>
  urls:
    - https://your-host.com/handler

Every 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:

proto
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 in config.yaml, selected with room_preset.
  • RoomInternal.webhooks — persist the resolved list. This is the analogue of EgressInfo.Request. Deliberately not on the public Room proto, since entries hold signing keys and Room is returned by ListRooms.
  • webhook.GetRoomNotifyOptions — mirror of egress.GetEgressNotifyOptions.

Usage would be:

jsonc
// 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.GetRoomNotifyOptions helper, 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, CreateRoom validation of URLs and signing keys, a config-sample.yaml note, 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.