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 -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 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 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 -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 -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 -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 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
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()
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
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()
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
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()
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
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
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
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']}")
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 retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 15 minutes |
| 4th retry | 1 hour |
| 5th retry | 4 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:
{
"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
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
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)
# 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