POSTs to your own HTTPS endpoints when tunnel
sessions open, close, or get rejected. Endpoints live on the shared
ClutchCall webhook plane (webhooks.* procedures) and are scoped to an
organization plus the tunnel vertical, so the Webhooks screen in the
tunnel console only ever shows and edits tunnel-facing endpoints.
This page covers the tunnel side: which event tokens exist, what the picker’s
event groups mean, when each session event fires, and how to reconcile what
actually landed on your receiver. Signature construction, retries, and the
delivery plane itself are shared across verticals.
Event types emitted by tunnel
The event tokens are not hardcoded in the console. The picker renders fromwebhooks.catalog, which returns the BFF’s webhook registry; the console reads
catalog[tunnel].groups. Because the picker can only offer what the registry
declares, it cannot offer a token that no producer emits.
Each group in the catalog has this shape:
So the authoritative list of tunnel event tokens for your deployment is the
union of
types across the tunnel groups. To read it outside the console:
webhooks.create and
webhooks.update run assertKnownEventTypes(...) on the submitted
eventTypes, so a typo’d or retired token is rejected rather than silently
stored as a subscription that never fires.
Two tokens are worth calling out because they do not come from session
traffic:
test.ping— what the Test button sends viawebhooks.sendTest. It is a fully signed delivery that appears in the delivery log like any other, so it is the right way to prove signature verification and network reachability before you wait for real sessions.*— not an event, a wildcard subscription. See below.
How event type matching works
The console applies exactly the matching contract the dispatcher applies. A subscription pattern covers an event type if:- the pattern equals the event type, or
- the pattern is
*, or - the pattern ends in
.*and the event type starts with the prefix before the*.
- Selecting every group collapses to
['*']. An endpoint stored as*is subscribed to everything in the tunnel registry including event types added to the registry later. If you want a frozen subscription set, deselect at least one group so the endpoint stores explicit tokens. - The “Event groups” column is derived, not stored. The row shows a group’s
label when the endpoint’s stored patterns cover any of that group’s types. An
endpoint stored as
*renders as every group label. Patterns that match no group at all are shown verbatim as raw tokens.
Session lifecycle: when each event fires
Tunnel session events mark the three outcomes a session can have, in the console’s own terms: a session opens, a session closes, or a session attempt is rejected.- Open fires when a session is admitted. It is the first event for a session id and marks the start of the session’s lifetime.
- Close fires when that session tears down. Paired with the open event for the same session, it bounds the session’s duration.
- Reject fires when an attempt is refused. A rejected attempt never became a session, so it is reported by the reject event alone — there is no open or close event to pair with it.
description text in the picker as the per-deployment source of
truth for what is included in each group; the console renders that string
directly from the registry.
Delivery is per-event and independent, and the delivery plane retries. Do not
assume your receiver sees open before close for a given session — order by the
timestamps in the payload, not by arrival order.
Subscribing an endpoint to session events
webhooks.create takes the org, the vertical (tunnel), the URL, an optional
description, the event tokens, and the private-egress flag. The console builds
eventTypes from the checked groups, collapsing to ['*'] when all are
checked.
Constraints the procedures enforce:
- URL scheme.
https://by default. Anhttp://URL is rejected withBAD_REQUESTunless the endpoint is created withallowPrivateEgress: true— the On-prem / private-network endpoint checkbox. That flag is also what permits RFC1918 targets. - Reachability. Both create and update call
assertDeliverableOrBadRequest(url, allowPrivateEgress), so an undeliverable target fails at configuration time rather than becoming a dead endpoint. - Lengths. URL up to 2048 characters; description up to 300.
- Scheme changes on update.
webhooks.updatevalidates a new URL against the storedallow_private_egressflag of that row, not against anything in the request. Moving an existing endpoint tohttp://therefore fails unless the row was created as on-prem.
create and by rotateSecret. The
row afterwards keeps only a sealed secret and a short
signing_secret_preview for identification. Deleting the endpoint clears the
reveal panel, because that secret belonged to the deleted endpoint.
Rotation is immediate and single-sided: rotateSecret seals the new secret in
place, and the previous secret stops verifying at once. Update your receiver to
accept the new secret before, or immediately as, you rotate.
Verifying a delivery
Every delivery carries a signature header whose value ist=<timestamp>,v1=hex(hmac_sha256(secret, "<t>.<body>")). The header name is
brand-specific; the console prints the exact header for your deployment in the
Signing secret panel when a secret is minted or rotated. HTTP header names
are case-insensitive, so match it case-insensitively in your receiver.
What the delivery log records
webhooks.deliveries.list backs the Recent deliveries table. The console
polls it every 15 s, requests 50 rows (the procedure defaults to 50 and accepts
up to 200 per page), and passes vertical: 'tunnel' so the log stays scoped to
tunnel endpoints plus org-wide endpoints rather than being filtered client-side.
Clicking an endpoint row filters the log to that endpointId.
Per delivery you get:
webhooks.deliveries.redeliver re-queues a single delivery; the console exposes
it for rows in delivered and dead state.
Endpoint health is tracked on the endpoint row, not the delivery: status
(active / paused / failing), consecutive_failures, last_delivery_at,
and last_status_code. Setting an endpoint back to active through
webhooks.update both resumes it and zeroes consecutive_failures, which is
how you clear a failing endpoint after fixing the receiver.
Worked example: session audit trail into a SIEM
The goal is a tamper-evident record of every session that opened, closed, or was rejected, with no gaps. 1. Create one endpoint subscribed to all tunnel groups. Leave every group checked so the endpoint is stored as['*'] and automatically picks up event
types added to the registry later — for an audit trail you want the superset,
not a frozen list.
allowPrivateEgress: true so an RFC1918 or http:// target is accepted.
2. Verify, then forward, then acknowledge. Reject anything that fails
signature verification, and return a non-2xx only when you genuinely failed to
persist — a non-2xx increments attempts and consecutive_failures, and a
persistently failing endpoint ends up failing.
test.ping through the real dispatcher and writes a row to the
delivery log, so a failure here tells you whether the problem is your signature
check (401 in last_status_code) or your network path (last_error with no
status code).
4. Reconcile. The delivery log is the reconciliation source. Filter it to
the SIEM endpoint, look for rows in dead state, fix the receiver, then set the
endpoint back to active (which clears consecutive_failures) and
Redeliver the dead rows. Because redelivery replays the same event, make
your SIEM ingest idempotent on the event’s own identifier rather than on arrival
order.
5. Rotate on a schedule. rotateSecret invalidates the old secret
immediately, so sequence it as: stage the new secret in the shim so it accepts
both, rotate in the console, copy the revealed secret in, drop the old one.
Rotations are audited (webhooks.endpoint.rotate_secret), as are endpoint
creates and updates.
Related
- Authentication — API keys for the control plane
- Telemetry — metrics and traces alongside webhook events

