Robotics (Optimus) webhook endpoints live on the same unified webhook plane as the rest of ClutchCall: they are org-scoped rows created through webhooks.create with vertical: 'fleet'. A fleet endpoint receives a signed POST when something changes on the robot graph — for example when a ROS2 endpoint is discovered on a robot that has just come online. Only the Optimus console mounts this screen. Arena (games) emits no webhook events, so there is no webhooks screen there.

Event groups and tokens

An endpoint subscribes to event-type tokens. Tokens are grouped for the picker, and both the groups and their tokens come from the server-side registry returned by webhooks.catalog, under the fleet key: The registry is the single source of truth in both directions:
  • The picker can only offer groups and tokens that a producer actually emits, because it renders straight from webhooks.catalog.
  • webhooks.create and webhooks.update run every submitted token through the same registry check. A token that is not in the registry is rejected with BAD_REQUEST, so a hand-written eventTypes array cannot subscribe to a typo.
To see the exact fleet groups and tokens for your org, call webhooks.catalog({ orgId }) and read fleet.groups, or open the Add endpoint card in the console — the two render the same data. One token exists outside the group picker: test.ping. The Test button on an endpoint row calls webhooks.sendTest, which delivers a signed test.ping to that endpoint immediately and reports the HTTP status it got back. Use it to prove signature verification works before you wait for real robot traffic.

How a subscription matches an event type

An endpoint stores a list of patterns in event_types. When an event is produced, the dispatcher matches it against each pattern using three rules: There is no deeper wildcard syntax: * in the middle of a token is not special, and a trailing .* only matches on the literal prefix before it. The console applies the same matching contract in reverse to label an existing endpoint. For each configured group it checks whether the endpoint’s patterns cover the group’s tokens, and shows the matching group labels as chips. If the endpoint stores *, every group label is shown. If the patterns cover no group cleanly — for example after you set eventTypes by hand — the raw tokens are shown instead.

Choosing between ’*’ and per-group subscriptions

The picker starts with every group ticked. When all groups are ticked, the console does not send the expanded token list; it collapses the selection to the single pattern ['*']. That distinction matters:
  • * (all groups ticked) — the endpoint receives event types that are added to the registry after you created it. Pick this for a general fleet middleware or event bus that forwards everything downstream and filters on its own side. Your receiver must tolerate unknown event types without failing the delivery.
  • Per-group tokens (some groups ticked) — the endpoint stores the explicit token list for the ticked groups, and nothing else. New event types added to the registry later will not be delivered until you update the endpoint. Pick this when the receiver switches on a closed set of types, or when a downstream system must not see unrelated fleet traffic.
Untick everything and the console still submits ['*'] — an endpoint with no subscription would never fire, so “none” is treated as “all”. To narrow an existing endpoint, call webhooks.update({ orgId, id, eventTypes }) with the token list you want; the registry check runs again on update.

Creating an endpoint

webhooks.create takes the destination, an optional description, the token list, and the private-egress flag:
Constraints enforced at create time:
  • The URL must be https:// unless allowPrivateEgress is set. An http:// URL without that flag is rejected with BAD_REQUEST.
  • The URL is checked for deliverability before the row is written. An address the delivery worker is not allowed to reach fails as BAD_REQUEST rather than becoming an endpoint that can never succeed.
  • url accepts up to 2000 characters; description up to 300.
  • Every entry in eventTypes must exist in the registry.
The response contains the signing secret in cleartext exactly once. The row stores a sealed copy plus a 12-character preview for display. Copy the secret when the console reveals it — reopening the screen will not show it again. webhooks.rotateSecret({ orgId, id }) mints a replacement and returns it once. The previous secret stops verifying immediately, so roll the new value into your receiver first, then rotate. Creates, updates and secret rotations are all written to the org audit log.

Verifying a delivery

Each delivery carries a signature header built from the endpoint’s secret:
where v1 is hmac_sha256(secret, "<t>.<raw request body>"). HTTP header names are case-insensitive, so match it case-insensitively rather than byte-for-byte. Verify against the raw body bytes, before any JSON parsing or re-serialisation, and compare digests in constant time. Also compare t against your own clock and reject deliveries whose timestamp is too far out for your tolerance — the signature alone does not stop a captured request from being replayed later.

Private-network endpoints

allowPrivateEgress is a security switch, not a convenience toggle. Setting it:
  • permits http:// destinations, and
  • permits RFC1918 / private-network targets, which the usual egress guard would otherwise refuse.
Cloud metadata addresses stay blocked whether or not the flag is set. Only set it for a receiver you operate inside your own network. The flag is stored on the endpoint row and governs later URL edits too: webhooks.update re-validates a new URL against the existing row’s flag, so an endpoint created without private egress cannot be edited into an http:// target. The console marks such endpoints with a private egress note under the URL.

Endpoint status and failure handling

Each endpoint row carries the state the console renders: webhooks.update({ orgId, id, status }) accepts active or paused. Pause stops deliveries while keeping the endpoint and its secret. Setting status: 'active' both resumes a paused endpoint and clears a failing endpoint: it resets consecutive_failures to zero, which is how you recover an endpoint that was auto-paused after repeated failures. webhooks.remove deletes the endpoint and deliveries stop immediately.

Delivery log and redelivery

webhooks.deliveries.list returns attempt records newest-first. It accepts endpointId, vertical, and status filters, plus limit (max 200, default 50) and page. Passing vertical: 'fleet' scopes the query server-side to fleet endpoints plus org-wide endpoints whose vertical is null — prefer that over fetching org-wide rows and filtering in your own code, which starves the fleet log on orgs with busy voice or streams traffic. Each row reports: Payload bodies are deliberately excluded from the list response — they are large, and the console loads a single delivery’s full payload only when you open its detail drawer. webhooks.deliveries.redeliver({ orgId, id }) re-queues one delivery. The console offers it for dead deliveries (recovering events lost while your receiver was down) and for delivered ones (replaying an event against a fixed receiver). Because a replay is a fresh attempt of the same event, your receiver must be idempotent.

Worked receiver example

An Express receiver that verifies the signature over the raw body and acknowledges the delivery:
Two things this example is doing deliberately:
  • It does not reject unknown event types. An endpoint subscribed with * will start receiving tokens added to the registry after it was created, and a receiver that throws on an unrecognised type turns every one of those into a retried failure and eventually a failing endpoint.
  • It treats redelivery as normal. The same event can arrive more than once after a retry or a manual Redeliver, so handleFleetEvent must key off the event’s identifier and be safe to run twice.
Start by pointing this receiver at a new endpoint and pressing Test in the console: webhooks.sendTest reports the HTTP status your receiver returned, so a signature bug shows up as a 401 in the result message instead of as silent missing traffic later.
  • Authentication — API keys and org scoping for control-plane calls
  • Telemetry — metrics and traces for the delivery path