// Platform

Webhooks

Subscribe an HTTPS endpoint to project events and receive signed POST deliveries. Every payload carries an HMAC-SHA256 over timestamp.body so the receiver can verify origin and reject replays.

Event Envelope

Every webhook delivery posts a JSON envelope with the following canonical shape. The data object varies by event type and carries the event-specific payload.

JSON
{ "id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2", "object": "event", "type": "batch.completed", "created_at": 1709000100, "data": { "...": "event-specific payload" } }
Field Type Description
id string Unique event identifier prefixed with evt_. Use for deduplication.
object string Always the literal string event.
type string Event type identifier (see Event Types).
created_at integer Unix timestamp (seconds since epoch) when the event was emitted.
data object Event-specific payload. Schema depends on type.

Budget alert deliveries (see Budget Alert Webhooks) use a legacy non-enveloped payload shape and do not include the id, object, or type fields documented here.

Event Types

Subscribe your webhook endpoints to any combination of the following event types. Types are grouped by family for readability; subscriptions are made against the full dotted identifier (for example exec.completed).

Event Type Description
exec.* XEM execution lifecycle and approval chain
exec.dispatched A XEM tool invocation was dispatched to an execution agent.
exec.completed A XEM tool invocation completed successfully.
exec.failed A XEM tool invocation failed with a non-zero exit code or transport error.
exec.timeout A XEM tool invocation exceeded its declared timeout_ms.
exec.ambiguous A XEM tool invocation target was ambiguous and needs operator resolution.
exec.approval_requested An approval request was enqueued for a destructive invocation.
exec.approved An approval request was approved by a privileged user.
exec.rejected An approval request was explicitly rejected by a privileged user.
exec.approval_escalated An approval request was escalated to the next contact in the escalation chain.
exec.approval_timed_out An approval request exceeded its timeout without a decision.
exec.agent_enrolled A new XEM execution agent completed enrollment and its capability manifest was accepted.
exec.agent_offline A XEM execution agent transitioned to offline after missing the lease-renewal deadline.
exec.discovery_pending A XEM discovery request was accepted and is awaiting operator action.
exec.workspace_created An operational workspace was created.
exec.workspace_budget_exhausted A workspace consumed its inference-token budget for the current window.
agent.* Agent lifecycle management
agent.created A new agent record was created via the management API.
agent.updated An existing agent record was updated via the management API.
agent.deleted An agent was soft-deleted via the management API.
agent.enrolled An agent finished enrollment and is eligible to receive dispatches.
agent.key_rotated An agent's CURVE transport keypair rotated and the new public key was persisted.
join_key.* Join key lifecycle
join_key.created A new join key was minted via the management API.
join_key.revoked An existing join key was revoked.
batch.* Batch jobs
batch.completed A batch job completed successfully.
batch.failed A batch job failed.
batch.cancelled A batch job was cancelled.
response.* Responses API
response.completed A response completed successfully.
response.failed A response failed.
file.* Files API
file.uploaded A file was uploaded.
file.deleted A file was deleted.
conversation.* Conversations
conversation.created A conversation was created.
conversation.deleted A conversation was deleted.
ox.alert.* Observability
ox.alert.raised A learning invariant was violated. Raised at critical severity.

API Endpoints

All paths are relative to your endpoint base URL: https://api.xerotier.ai/proj_ABC123/ENDPOINT_SLUG

Method Path Description
POST /v1/webhooks Create a webhook endpoint
GET /v1/webhooks List webhook endpoints
GET /v1/webhooks/{webhook_id} Get a webhook endpoint
PUT /v1/webhooks/{webhook_id} Update a webhook endpoint
DELETE /v1/webhooks/{webhook_id} Delete a webhook endpoint
POST /v1/webhooks/{webhook_id}/test Send a test delivery
GET /v1/webhooks/{webhook_id}/deliveries List delivery history

Create Webhook

POST /v1/webhooks

Parameter Type Description
urlrequired string HTTPS URL to receive webhook deliveries. Must use the https:// scheme.
eventsrequired array Array of event type strings to subscribe to. At least one event is required.
descriptionoptional string Human-readable description of the webhook endpoint.
metadataoptional object Up to 16 key-value pairs of custom metadata.
curl
curl -X POST https://api.xerotier.ai/proj_ABC123/my-endpoint/v1/webhooks \ -H "Authorization: Bearer xero_my-project_abc123" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/webhook", "events": ["batch.completed", "response.completed"], "description": "Production webhook", "metadata": {"env": "production"} }'

Response

{ "id": "00000000-1111-0000-1111-000000000000", "object": "webhook_endpoint", "url": "https://example.com/webhook", "description": "Production webhook", "secret": "whsec_a1b2c3d4e5f6...", "events": ["batch.completed", "response.completed"], "is_active": true, "metadata": {"env": "production"}, "created_at": 1709000000, "updated_at": 1709000000 }

The secret field is returned only in the creation response. Store it securely, it cannot be retrieved again. You will need it to verify webhook signatures.

List Webhooks

GET /v1/webhooks

Returns a paginated list of webhook endpoints. Use limit and after for pagination.

curl
curl https://api.xerotier.ai/proj_ABC123/my-endpoint/v1/webhooks?limit=10 \ -H "Authorization: Bearer xero_my-project_abc123"

Get Webhook

GET /v1/webhooks/{webhook_id}

curl
curl https://api.xerotier.ai/proj_ABC123/my-endpoint/v1/webhooks/00000000-1111-0000-1111-000000000000 \ -H "Authorization: Bearer xero_my-project_abc123"

Update Webhook

PUT /v1/webhooks/{webhook_id}

All fields are optional. Only provided fields are updated.

Parameter Type Description
urloptional string Updated HTTPS URL.
eventsoptional array Updated event subscriptions. Must be non-empty if provided.
descriptionoptional string Updated description.
is_activeoptional boolean Enable or disable the webhook endpoint.
metadataoptional object Updated metadata (replaces existing metadata).
curl
curl -X PUT https://api.xerotier.ai/proj_ABC123/my-endpoint/v1/webhooks/00000000-1111-0000-1111-000000000000 \ -H "Authorization: Bearer xero_my-project_abc123" \ -H "Content-Type: application/json" \ -d '{ "events": ["batch.completed", "batch.failed", "response.completed"], "is_active": true }'

Delete Webhook

DELETE /v1/webhooks/{webhook_id}

Permanently deletes the webhook endpoint and all associated delivery records. Returns 200 OK with a JSON deletion confirmation body (not 204 No Content).

curl
curl -X DELETE https://api.xerotier.ai/proj_ABC123/my-endpoint/v1/webhooks/00000000-1111-0000-1111-000000000000 \ -H "Authorization: Bearer xero_my-project_abc123"

Test Webhook

POST /v1/webhooks/{webhook_id}/test

Sends a test delivery to the webhook endpoint with a synthetic payload. Returns the HTTP status and response body from your endpoint.

curl
curl -X POST https://api.xerotier.ai/proj_ABC123/my-endpoint/v1/webhooks/00000000-1111-0000-1111-000000000000/test \ -H "Authorization: Bearer xero_my-project_abc123"

Response

{ "success": true, "http_status": 200, "response_body": "OK", "error_message": null }

List Deliveries

GET /v1/webhooks/{webhook_id}/deliveries

Returns a paginated list of delivery attempts for the webhook endpoint.

curl
curl https://api.xerotier.ai/proj_ABC123/my-endpoint/v1/webhooks/00000000-1111-0000-1111-000000000000/deliveries?limit=20 \ -H "Authorization: Bearer xero_my-project_abc123"

Response

{ "object": "list", "data": [ { "id": "del_abc123", "object": "webhook_delivery", "event_type": "batch.completed", "http_status": 200, "response_body": "OK", "status": "delivered", "attempt_count": 1, "created_at": 1709000100 } ], "has_more": false }

Client Examples

The following examples show how to manage webhooks using Python and Node.js. All examples assume you have set your API key and base URL.

Create Webhook

Python (requests)
import requests headers = {"Authorization": "Bearer xero_my-project_abc123"} # Create webhook response = requests.post( "https://api.xerotier.ai/proj_ABC123/my-endpoint/v1/webhooks", headers=headers, json={ "url": "https://example.com/webhook", "events": ["response.completed", "batch.completed"] } ) webhook = response.json()
Node.js (fetch)
const headers = { "Authorization": "Bearer xero_my-project_abc123", "Content-Type": "application/json" }; // Create webhook const response = await fetch( "https://api.xerotier.ai/proj_ABC123/my-endpoint/v1/webhooks", { method: "POST", headers, body: JSON.stringify({ url: "https://example.com/webhook", events: ["response.completed", "batch.completed"] }) } ); const webhook = await response.json();

List Webhooks

Python (requests)
import requests headers = {"Authorization": "Bearer xero_my-project_abc123"} response = requests.get( "https://api.xerotier.ai/proj_ABC123/my-endpoint/v1/webhooks", headers=headers ) webhooks = response.json()
Node.js (fetch)
const response = await fetch( "https://api.xerotier.ai/proj_ABC123/my-endpoint/v1/webhooks", { headers: { "Authorization": "Bearer xero_my-project_abc123" } } ); const webhooks = await response.json();

Update Webhook

Python (requests)
import requests headers = {"Authorization": "Bearer xero_my-project_abc123"} webhook_id = "00000000-1111-0000-1111-000000000000" response = requests.put( f"https://api.xerotier.ai/proj_ABC123/my-endpoint/v1/webhooks/{webhook_id}", headers=headers, json={ "events": ["batch.completed", "batch.failed", "response.completed"], "is_active": True } ) updated = response.json()
Node.js (fetch)
const webhookId = "00000000-1111-0000-1111-000000000000"; const response = await fetch( `https://api.xerotier.ai/proj_ABC123/my-endpoint/v1/webhooks/${webhookId}`, { method: "PUT", headers: { "Authorization": "Bearer xero_my-project_abc123", "Content-Type": "application/json" }, body: JSON.stringify({ events: ["batch.completed", "batch.failed", "response.completed"], is_active: true }) } ); const updated = await response.json();

Delete Webhook

Python (requests)
import requests headers = {"Authorization": "Bearer xero_my-project_abc123"} webhook_id = "00000000-1111-0000-1111-000000000000" response = requests.delete( f"https://api.xerotier.ai/proj_ABC123/my-endpoint/v1/webhooks/{webhook_id}", headers=headers ) # Returns 200 OK with a deletion confirmation body on success
Node.js (fetch)
const webhookId = "00000000-1111-0000-1111-000000000000"; const response = await fetch( `https://api.xerotier.ai/proj_ABC123/my-endpoint/v1/webhooks/${webhookId}`, { method: "DELETE", headers: { "Authorization": "Bearer xero_my-project_abc123" } } ); // Returns 200 OK with a deletion confirmation body on success

Test Webhook

Python (requests)
import requests headers = {"Authorization": "Bearer xero_my-project_abc123"} webhook_id = "00000000-1111-0000-1111-000000000000" response = requests.post( f"https://api.xerotier.ai/proj_ABC123/my-endpoint/v1/webhooks/{webhook_id}/test", headers=headers ) result = response.json() print(f"Test delivery: success={result['success']}, status={result['http_status']}")
Node.js (fetch)
const webhookId = "00000000-1111-0000-1111-000000000000"; const response = await fetch( `https://api.xerotier.ai/proj_ABC123/my-endpoint/v1/webhooks/${webhookId}/test`, { method: "POST", headers: { "Authorization": "Bearer xero_my-project_abc123" } } ); const result = await response.json(); console.log(`Test delivery: success=${result.success}, status=${result.http_status}`);

Security

HMAC-SHA256 Payload Signing

Every webhook delivery includes a signature header so you can verify the payload was sent by Xerotier and has not been tampered with.

The following headers are included with every delivery:

Header Description
X-Webhook-ID Unique identifier for this delivery. Use for deduplication.
X-Webhook-Timestamp Unix timestamp (seconds) when the delivery was sent.
X-Webhook-Signature HMAC-SHA256 signature in the format sha256=HEX_DIGEST.

Signature Computation

The signature is computed as:

Formula
HMAC-SHA256(key=secret, message="{timestamp}.{payload}")

Where timestamp is the value of the X-Webhook-Timestamp header and payload is the raw JSON request body. The result is hex-encoded and prefixed with sha256=.

HTTPS Only

Webhook URLs must use the https:// scheme. HTTP URLs and private/internal network addresses are rejected to prevent SSRF attacks.

Retry Policy

Failed deliveries (non-2xx responses or network errors) are retried with a fixed exponential backoff schedule. The initial delivery is attempt 1; each subsequent retry is scheduled relative to the preceding failure:

Retry Delay After Previous Failure
1st retry1 minute
2nd retry5 minutes
3rd retry15 minutes
4th retry1 hour
5th retry4 hours

A delivery may be attempted up to 6 times total (1 initial attempt plus up to 5 retries). After the 5th retry fails, the delivery is marked as permanently failed. The total retry window spans approximately 5 hours and 21 minutes from the initial failure.

Your endpoint should respond with a 2xx status code within 30 seconds, which is the per-attempt delivery timeout enforced by the router.

Limits

Limit Value
Webhook endpoints per project20
Metadata key-value pairs per endpoint16
Delivery response timeout30 seconds
Test deliveries per webhook per hour10
URL schemehttps:// only, private network addresses rejected

Budget Alert Webhooks

Budget alerts can also send HTTP POST notifications to a configured webhook URL when spending thresholds are reached. See Budget Alerts for full configuration details.

Webhook Payload

Budget-alert deliveries use a legacy non-enveloped payload shape that predates the canonical event envelope and does not include the id/object/type fields. When a budget threshold is triggered, the webhook receives:

JSON
{ "eventType": "budget.threshold_reached", "alertId": "uuid", "alertName": "Monthly API Budget", "projectId": "uuid", "projectName": "My Project", "threshold": 75, "currentSpendCents": 7500, "budgetAmountCents": 10000, "spendPercentage": 75.0, "timestamp": "2026-01-15T10:30:00Z" }

Signature Verification

Python

Python
import hashlib import hmac import time def verify_webhook(payload_body, secret, signature_header, timestamp_header): """Verify the HMAC-SHA256 signature of a webhook delivery.""" # Check timestamp is within 5 minutes to prevent replay attacks timestamp = int(timestamp_header) if abs(time.time() - timestamp) > 300: raise ValueError("Timestamp too old") # Compute expected signature message = f"{timestamp}.{payload_body}".encode("utf-8") expected = hmac.new( secret.encode("utf-8"), message, hashlib.sha256 ).hexdigest() expected_sig = f"sha256={expected}" if not hmac.compare_digest(expected_sig, signature_header): raise ValueError("Invalid signature") return True # Usage in a Flask handler: # verify_webhook( # request.data.decode("utf-8"), # "whsec_your_secret", # request.headers["X-Webhook-Signature"], # request.headers["X-Webhook-Timestamp"] # )

Node.js

Node.js
import crypto from "crypto"; function verifyWebhook(payloadBody, secret, signatureHeader, timestampHeader) { // Check timestamp is within 5 minutes to prevent replay attacks const timestamp = parseInt(timestampHeader, 10); const now = Math.floor(Date.now() / 1000); if (Math.abs(now - timestamp) > 300) { throw new Error("Timestamp too old"); } // Compute expected signature const message = `${timestamp}.${payloadBody}`; const expected = crypto .createHmac("sha256", secret) .update(message) .digest("hex"); const expectedSig = `sha256=${expected}`; if (!crypto.timingSafeEqual( Buffer.from(expectedSig), Buffer.from(signatureHeader) )) { throw new Error("Invalid signature"); } return true; } // Usage in an Express handler: // app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => { // verifyWebhook( // req.body.toString(), // "whsec_your_secret", // req.headers["x-webhook-signature"], // req.headers["x-webhook-timestamp"] // ); // res.sendStatus(200); // });

curl (manual check)

Bash
# Given a payload file and the webhook secret: TIMESTAMP="1709000100" PAYLOAD=$(cat payload.json) SECRET="whsec_your_secret" # Compute the expected signature EXPECTED=$(printf '%s.%s' "$TIMESTAMP" "$PAYLOAD" | \ openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}') echo "sha256=$EXPECTED" # Compare with the X-Webhook-Signature header value