Webhooks

Receive HTTP notifications when events occur in your project.

Overview

Webhooks allow your application to receive real-time HTTP POST notifications when specific events occur in your Xerotier.ai project. You can create webhook endpoints, subscribe to event types, and track delivery history through the Webhooks API.

All webhook deliveries are signed with HMAC-SHA256 using a per-endpoint secret, enabling your application to verify that payloads originated from Xerotier.

Event Types

Subscribe your webhook endpoints to any combination of the following event types:

Event Type Description
batch.completed A batch job completed successfully.
batch.failed A batch job failed.
batch.cancelled A batch job was cancelled.
response.completed A response completed successfully.
response.failed A response failed.
file.uploaded A file was uploaded.
file.deleted A file was deleted.
conversation.created A conversation was created.
conversation.deleted A conversation was deleted.

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/{id} Get a webhook endpoint
PUT /v1/webhooks/{id} Update a webhook endpoint
DELETE /v1/webhooks/{id} Delete a webhook endpoint
POST /v1/webhooks/{id}/test Send a test delivery
GET /v1/webhooks/{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 }

Important: 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.

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", "success": true, "attempt_number": 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/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/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/v1/webhooks", headers=headers ) webhooks = response.json()
Node.js (fetch)
const response = await fetch( "https://api.xerotier.ai/proj_ABC123/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/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/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/v1/webhooks/{webhook_id}", headers=headers ) # Returns 204 No Content on success
Node.js (fetch)
const webhookId = "00000000-1111-0000-1111-000000000000"; const response = await fetch( `https://api.xerotier.ai/proj_ABC123/v1/webhooks/${webhookId}`, { method: "DELETE", headers: { "Authorization": "Bearer xero_my-project_abc123" } } ); // Returns 204 No Content 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/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/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:

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 exponential backoff. The schedule is:

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

After 5 failed retry attempts, the delivery is marked as permanently failed. Your endpoint should respond with a 2xx status code within 10 seconds.

Limits

  • Max 20 webhook endpoints per project.
  • Max 16 metadata key-value pairs per endpoint.
  • HTTPS URLs only -- HTTP and private network addresses are rejected.
  • 10-second timeout for delivery responses.
  • Max 10 test deliveries per webhook per hour.

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

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