webhooks.* tRPC router backs every product, and realtime endpoints are
rows in the shared webhook_endpoint table with vertical = 'realtime'.
ClutchCall POSTs signed event batches to the URLs you register, and
records every attempt in a delivery log you can page through and replay.
Endpoints are scoped to your organization, not to a single app. An
endpoint may optionally be narrowed to one realtime app.
Event groups and the event types they cover
You do not type event names by hand.webhooks.catalog returns the
registry of groups that the backend actually emits, keyed by vertical.
Read catalog.realtime.groups. Each group carries:
Realtime currently registers the existence and presence groups,
and the console pre-selects both when you open the add form.
The registry is the source of truth. An event type that no emitter
produces is removed from it, so the picker cannot offer a dead
subscription.
client_event and cache_miss were offered by an earlier
console build and produced nothing; they are no longer in the registry.
If you have an old endpoint subscribed to those strings, it receives
nothing — re-pick the groups you want.
If the catalog fails to load, creation is blocked rather than defaulted.
An endpoint is never created with an empty or guessed subscription.
Choosing groups versus subscribing to all events
The stored subscription is a list of patterns inevent_types. The
dispatcher matches an outgoing event against those patterns with three
rules:
- exact match —
presence.member_added - the wildcard
*— matches every event type - a prefix glob ending in
.*—presence.*matches any type starting withpresence.
types. Selecting every group collapses to the single pattern *.
The difference matters over time: a fixed list receives only the types
that existed when you created the endpoint, while * also receives
event types added to the registry later.
Pick specific groups when your handler switches exhaustively on event
type and would reject or misroute an unknown one. Pick * when your
handler ignores types it does not recognise.
In the endpoint list, the Event groups column reverses this mapping:
an endpoint on * shows every group label, an endpoint on concrete
types shows the labels of the groups those types belong to, and anything
that maps to no known group is shown as the raw patterns.
App-scoped and org-wide endpoints
Two different scopes appear on this screen. Do not confuse them. Scope to app (resourceRef on create) narrows a realtime endpoint to
one app. Leave it as All apps and resourceRef is null, so every
realtime app in the org delivers to that URL. When set, it holds the
app’s public appId, and the endpoint row shows app <appId> under the
URL.
Org-wide endpoints are rows whose vertical is null. These are not
realtime-specific — they are shared with the other products, so voice and
streams deliver to them too. The console tags them org-wide and warns
you before pausing or deleting one, because the change affects those
other products’ deliveries as well. Endpoints you create from this
screen are always vertical = 'realtime' and affect realtime only.
webhooks.list with vertical: 'realtime' returns both realtime rows
and the org-wide rows that will also receive realtime events.
Signing secret and signature headers
webhooks.create returns the signing secret once, in the creation
response. It is sealed at rest afterwards, and the endpoint row exposes
only signing_secret_preview — a truncated prefix for identifying which
secret a row holds. Store the full value when it is shown.
Every request is signed with HMAC-SHA256 over the raw request body and
carries two headers with the same signature:
Verify before you trust the payload:
Rotating the signing secret
webhooks.rotateSecret mints a new secret and returns it once, exactly
like create. The current secret stops verifying immediately — there is
no overlap window — so rotation is a cutover, not a gradual roll:
- Deploy a handler that accepts either the old or the new secret, or accept a short window of rejected deliveries.
- Call
webhooks.rotateSecret, or press Rotate on the row and confirm. - Store the new secret and drop the old one from your handler.
- Replay anything that failed during the cutover from the delivery log.
Endpoint states and consecutive failures
Each endpoint row carries astatus and a failure counter.
consecutive_failures counts unsuccessful deliveries in a row; the
console prints it under the status pill whenever it is above zero. A
successful delivery is what clears it. The row also shows
last_delivery_at and last_status_code, which is usually enough to tell
“my endpoint is down” from “my endpoint is rejecting the signature”.
Pause and resume are the same call with a different status:
webhooks.remove deletes the endpoint and deliveries to it stop
immediately. Prefer pausing while you debug — a paused endpoint keeps its
subscription, its secret and its delivery history.
The delivery log
webhooks.deliveries.list is scoped server-side. Pass
vertical: 'realtime' and you get realtime deliveries plus deliveries to
org-wide endpoints, already filtered. There is no client-side filtering
to do, and a busy voice or streams tenant cannot crowd realtime rows out
of the page. Pass an endpointId to pin the log to one endpoint; the
vertical filter is ignored in that case.
The list is paginated with limit and page. The console requests 50
rows per page and re-polls every 15 seconds.
List responses deliberately omit the fat fields. To see the full request
payload and last_error for one attempt, call
webhooks.deliveries.get({ orgId, id }) — this is what the console’s
per-delivery detail drawer does.
Redelivery and duplicate handling
webhooks.deliveries.redeliver({ orgId, id }) queues an existing
delivery to be sent again. It returns as soon as the delivery is queued,
not when your endpoint answers, so watch the log for the new attempt.
Use it after you have fixed the cause: redeploy the handler, rotate to a
secret both sides agree on, or resume a paused endpoint, then replay the
failed deliveries.
Because a delivery can be replayed — by you, from this screen — your
handler must be idempotent. Key on the event’s identity and make a repeat
of an event you have already processed a no-op rather than a second
write.
Testing an endpoint
webhooks.sendTest({ orgId, id }) sends a signed test.ping to the
endpoint immediately. It is signed with the endpoint’s current secret, so
it exercises your verification path, not just your routing.
The result tells you what happened at the HTTP level:
No
statusCode at all means the request never got an HTTP response —
DNS, TLS, or connectivity — rather than a rejection by your handler.
Send a test right after creating an endpoint and again after every
secret rotation.
Organizing endpoints with tags
Endpoint rows are taggable. Because they live in the unifiedwebhook_endpoint table, they tag under the webhook_endpoint resource
kind rather than a realtime-specific one, so a tag you apply here is
visible on the same endpoint from the other products’ consoles.
Related
- Authentication — API keys and org scoping
- Telemetry — the other operational data streams

