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 bywebhooks.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.createandwebhooks.updaterun every submitted token through the same registry check. A token that is not in the registry is rejected withBAD_REQUEST, so a hand-writteneventTypesarray cannot subscribe to a typo.
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 inevent_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.
['*'] — 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:
- The URL must be
https://unlessallowPrivateEgressis set. Anhttp://URL without that flag is rejected withBAD_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_REQUESTrather than becoming an endpoint that can never succeed. urlaccepts up to 2000 characters;descriptionup to 300.- Every entry in
eventTypesmust exist in the registry.
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: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.
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:- 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 afailingendpoint. - It treats redelivery as normal. The same event can arrive more than once
after a retry or a manual Redeliver, so
handleFleetEventmust key off the event’s identifier and be safe to run twice.
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.
Related
- Authentication — API keys and org scoping for control-plane calls
- Telemetry — metrics and traces for the delivery path

