Webhook Events
Every execution lifecycle event the router fans out over the exec.* namespace. Same envelope, same HMAC-SHA256 signing, same retry schedule as the rest of the webhook surface. Subscribe with the standard create endpoint, verify the signature, dedupe on event id.
// nav: g e envelope · g x execution · g a approvals · g s signing · g d delivery · ? chip
Overview
Snake_case on the wire. All
exec.* payload field names are emitted in
snake_case (for example invocation_id,
tool_name, workspace_id). The JSON
examples below show the exact field names that subscribers
will receive.
| Event | Status | Description |
|---|---|---|
exec.dispatched | active | Tool invocation forwarded to a XEM agent. |
exec.completed | active | Tool invocation finished successfully. |
exec.failed | active | Tool invocation failed with a non-zero exit code or transport error. |
exec.timeout | active | Tool invocation exceeded its declared timeout_ms. |
exec.ambiguous | reserved | Reserved event type; no payload is emitted at this time. |
exec.approval_requested | active | An approval request was enqueued for a destructive invocation. |
exec.approved | active | Approval request granted by a privileged user. |
exec.rejected | active | Approval request explicitly rejected. |
exec.approval_escalated | active | Approval escalation chain advanced to the next stage. |
exec.approval_timed_out | active | Approval request exceeded its configured timeout. |
exec.agent_enrolled | active | A XEM execution agent completed enrollment. |
exec.agent_offline | active | A XEM agent lost its lease and went offline. |
exec.discovery_pending | reserved | Reserved event type; no payload is emitted at this time. |
exec.workspace_created | reserved | Reserved event type; no payload is emitted at this time. |
exec.workspace_budget_exhausted | reserved | Reserved event type; no payload is emitted at this time. |
Event types marked reserved are declared in the router enum but no payload is currently produced. Subscribers may register for them without error, but no deliveries will fire until the router begins emitting them.
Event Envelope
Every exec.* delivery uses the canonical webhook
envelope. The data object varies by event type and
is documented per-event below.
{
"id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2",
"object": "event",
"type": "exec.completed",
"created_at": 1709000100,
"data": { }
}
| Field | Type | Description |
|---|---|---|
id | string | Unique event identifier prefixed with evt_ (24 hex characters). Suitable for deduplication. |
object | string | Always the literal string event. |
type | string | Event type identifier (for example exec.completed). |
created_at | integer | Unix timestamp (seconds since epoch) when the event was emitted. |
data | object | Event-specific payload. Schema depends on type. |
The project id and webhook delivery id are not
placed in the JSON body. The delivery id is carried in the
X-Webhook-ID HTTP header; the owning project is
determined from the subscription rather than per-event metadata.
End-to-End Wire Example
The following is a complete exec.completed delivery
exactly as a subscriber endpoint will receive it.
POST /xerotier HTTP/1.1
Host: ops-ingest.example.com
Content-Type: application/json
User-Agent: Xerotier-Webhooks/1.0
X-Webhook-ID: 0f8c4a1e-1c2d-4a9b-9c1f-2e3d4a5b6c7d
X-Webhook-Timestamp: 1709000100
X-Webhook-Signature: sha256=9c1a...e3f0
{
"id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2",
"object": "event",
"type": "exec.completed",
"created_at": 1709000100,
"data": {
"invocation_id": "inv_01HXXXX",
"status": "success",
"duration_ms": 45200,
"exit_code": 0,
"completed_at": "2026-04-13T14:58:48.612Z"
}
}
Execution Events
Each execution lifecycle event has its own payload schema. The field set is intentionally minimal; richer state is fetched on demand via the invocation read endpoint.
exec.dispatched
{
"invocation_id": "inv_01HXXXX",
"tool_name": "kubectl_drain",
"workspace_id": "ws_01HXXXX",
"agent_id": "agt_01HXXXX",
"risk": "destructive",
"dispatched_at": "2026-04-13T14:58:03.412Z"
}
| Field | Type | Description |
|---|---|---|
invocation_id | string | Unique invocation identifier. |
tool_name | string | Tool being invoked. |
workspace_id | string | Workspace the tool executes in. |
agent_id | string | Agent assigned to the invocation. |
risk | string | Risk classification declared by the tool manifest. |
dispatched_at | string | ISO 8601 timestamp when dispatch occurred. |
exec.completed
{
"invocation_id": "inv_01HXXXX",
"status": "success",
"duration_ms": 45200,
"exit_code": 0,
"completed_at": "2026-04-13T14:58:48.612Z"
}
| Field | Type | Description |
|---|---|---|
invocation_id | string | Unique invocation identifier. |
status | string | Terminal status (for example success). |
duration_ms | integer | Wall-clock execution duration in milliseconds. |
exit_code | integer | Process exit code returned by the tool. |
completed_at | string | ISO 8601 completion timestamp. |
exec.failed
{
"invocation_id": "inv_01HXXXX",
"error_code": "tool_exit_nonzero",
"error_message": "kubectl drain returned exit code 1",
"failed_at": "2026-04-13T14:58:48.612Z"
}
| Field | Type | Description |
|---|---|---|
invocation_id | string | Unique invocation identifier. |
error_code | string | Machine-readable error code. |
error_message | string | Human-readable error description. |
failed_at | string | ISO 8601 timestamp when the failure was recorded. |
exec.timeout
{
"invocation_id": "inv_01HXXXX",
"timeout_ms": 60000,
"elapsed_ms": 60214,
"timed_out_at": "2026-04-13T14:59:03.626Z"
}
| Field | Type | Description |
|---|---|---|
invocation_id | string | Unique invocation identifier. |
timeout_ms | integer | Configured timeout threshold in milliseconds. |
elapsed_ms | integer | Actual elapsed time before timeout was declared. |
timed_out_at | string | ISO 8601 timestamp when the timeout was recorded. |
Approval Events
Approval-chain events fire as a destructive invocation moves through request, decision, escalation, or timeout. Each event carries only the fields relevant to its stage.
exec.approval_requested
{
"invocation_id": "inv_01HXXXX",
"approval_id": "app_01HXXXX",
"tool_name": "kubectl_drain",
"risk": "destructive",
"workspace_id": "ws_01HXXXX",
"requested_at": "2026-04-13T14:58:00.000Z"
}
| Field | Type | Description |
|---|---|---|
invocation_id | string | Invocation that triggered the approval request. |
approval_id | string | Unique approval-request identifier, suitable as a routing key. |
tool_name | string | Tool whose invocation is gated on this approval. |
risk | string | Risk classification declared by the tool manifest (for example destructive). |
workspace_id | string | Workspace the gated invocation targets. |
requested_at | string | ISO 8601 timestamp when the approval request was enqueued. |
exec.approved
{
"invocation_id": "inv_01HXXXX",
"approval_id": "app_01HXXXX",
"decided_by": "usr_01HXXXX",
"approved_at": "2026-04-13T14:59:12.001Z"
}
| Field | Type | Description |
|---|---|---|
invocation_id | string | Invocation released for dispatch. |
approval_id | string | Approval request that was granted. |
decided_by | string | User identifier of the privileged user who granted the request. |
approved_at | string | ISO 8601 timestamp when the decision was recorded. |
exec.rejected
{
"invocation_id": "inv_01HXXXX",
"approval_id": "app_01HXXXX",
"reason": "out of change window",
"rejected_at": "2026-04-13T14:59:12.001Z"
}
| Field | Type | Description |
|---|---|---|
invocation_id | string | Invocation that was blocked from dispatch. |
approval_id | string | Approval request that was rejected. |
reason | string | Free-form human-readable rejection reason supplied by the decider. |
rejected_at | string | ISO 8601 timestamp when the rejection was recorded. |
exec.approval_escalated
{
"invocation_id": "inv_01HXXXX",
"approval_id": "app_01HXXXX",
"stage": "stage-2",
"target": "oncall-platform",
"escalated_at": "2026-04-13T15:03:00.000Z"
}
| Field | Type | Description |
|---|---|---|
invocation_id | string | Invocation whose approval chain advanced. |
approval_id | string | Approval request whose chain advanced. |
stage | string | Stage identifier the chain advanced to (escalation policy defined). |
target | string | Group or rotation now responsible for the decision. |
escalated_at | string | ISO 8601 timestamp when the escalation fired. |
exec.approval_timed_out
{
"invocation_id": "inv_01HXXXX",
"approval_id": "app_01HXXXX",
"timed_out_at": "2026-04-13T15:13:00.000Z"
}
| Field | Type | Description |
|---|---|---|
invocation_id | string | Invocation that was blocked from dispatch by the timeout. |
approval_id | string | Approval request that expired without a decision. |
timed_out_at | string | ISO 8601 timestamp when the timeout was recorded. |
The exec.approval_requested event is the trigger
for approval-delegation patterns (forwarding to Slack,
PagerDuty, or an internal approval system). Any downstream
system that re-emits the payload must verify
the original X-Webhook-Signature before trusting
its contents; see Signature
Verification.
Agent Events
exec.agent_enrolled
{
"agent_id": "agt_01HXXXX",
"registration_name": "fleet-ops-sfo1",
"project_id": "proj_01HXXXX",
"manifest_hash": "sha256:b5a0...c91d",
"enrolled_at": "2026-04-13T14:58:03.412Z"
}
| Field | Type | Description |
|---|---|---|
agent_id | string | Unique agent identifier. |
registration_name | string | Human-readable name declared in the agent manifest. |
project_id | string | UUID of the project that owns the agent. |
manifest_hash | string | SHA-256 hash of the accepted capability manifest. |
enrolled_at | string | ISO 8601 timestamp when enrollment completed. |
exec.agent_offline
{
"invocation_id": "inv_01HXXXX",
"agent_id": "agt_01HXXXX",
"reason": "lease_expired",
"detected_at": "2026-04-13T15:00:12.000Z"
}
| Field | Type | Description |
|---|---|---|
invocation_id | string | Invocation that surfaced the offline transition. |
agent_id | string | Agent that went offline. |
reason | string | Detection reason (for example lease_expired). |
detected_at | string | ISO 8601 timestamp when the offline state was detected. |
Workspace Events
exec.workspace_created and
exec.workspace_budget_exhausted are declared in the
router event-type enum but are not currently emitted. The
payload schema is reserved and will be documented here when the
router begins producing these events. Subscribers may register
for them today; no deliveries will fire until the emitter ships.
Discovery Events
exec.discovery_pending is declared in the router
event-type enum but is not currently emitted. The payload
schema is reserved and will be documented here when the router
begins producing this event.
Subscribing to Events
Webhook endpoints are created with the standard
POST /v1/webhooks request shape. The
events array selects which event types are
delivered; there is no per-endpoint workspace or risk filter
today, so subscribers receive every delivery for the event
types they list.
{
"url": "https://ops-ingest.example.com/xerotier",
"events": ["exec.approval_requested", "exec.approval_escalated"],
"description": "On-call approval bridge",
"metadata": { "owner": "platform-oncall" }
}
For finer-grained routing, inspect
data.workspace_id and data.risk
on receipt and drop or re-route downstream. See the
Webhooks API reference for
the complete create, update, list, and delete surface.
Signature Verification
Every delivery includes three HTTP headers that let subscribers authenticate the payload:
| Header | Description |
|---|---|
X-Webhook-ID | UUID of the delivery record in webhook_deliveries. Use for delivery tracing. |
X-Webhook-Timestamp | Unix epoch (seconds) when the router signed the request. |
X-Webhook-Signature | HMAC-SHA256 signature in the format sha256=<hex-digest>. |
The signature is computed as
HMAC_SHA256(secret, timestamp + "." + raw_body),
hex-encoded and prefixed with sha256=. The
secret is returned exactly once at endpoint create
time. See the Webhooks API reference
for verification snippets in Python, Node, and shell.
Delivery & Retry
- Deliveries retry on non-2xx responses or network errors using a fixed exponential backoff schedule: 1 min, 5 min, 15 min, 1 hr, 4 hr. A delivery is attempted up to 6 times total (1 initial attempt plus up to 5 retries).
- Per-attempt delivery timeout is 30 seconds; endpoints that exceed this are treated as a failed attempt and re-scheduled per the backoff above.
- Ordering is not guaranteed. Subscribers
should dedupe on the envelope
idand sort bycreated_at. - At-least-once delivery. The envelope
idis suitable as a dedup key. - Delivery records (request payload, response status, response
body up to 1 KiB) surface in
GET /v1/webhooks/:id/deliveries.
For the full retry table, signing math, and limits, see the Webhooks API reference.