Webhooks

Subscribe to events: request completed, error, budget alert, key created, fine-tuning job done, and more.

Overview

Webhooks deliver events from AllRoutes to your HTTP endpoint as they happen. Use them to:

  • Get alerted when a key approaches its monthly budget
  • Trigger downstream pipelines when a fine-tuning job completes
  • Log every error to your incident system
  • Update internal dashboards on every chat completion

Webhooks differ from Broadcast in that they're event-driven (not high-throughput trace streaming) and signed for tamper-evidence. For per-request tracing at scale, use Broadcast; for selective events, use Webhooks.

Event Types

EventFires When
completion.successA chat completion succeeds (sampled)
completion.errorA chat completion fails (4xx or 5xx)
budget.thresholdA key crosses 50%, 80%, or 100% of its daily/monthly budget
budget.exhaustedA key hits its budget cap (402)
key.created / key.deleted / key.updatedAPI key lifecycle
provider_key.invalidA BYOK provider key fails validation
endpoint.unhealthyA self-hosted endpoint fails health checks
fine_tuning.job.succeeded / .failedFine-tuning job lifecycle
guardrail.triggeredA guardrail (PII, injection, toxicity) blocked a request
cache.purgedThe cache was manually purged

Subscribing

POST https://api.allroutes.ai/v1/webhooks

Request Body

FieldTypeRequiredDescription
urlstringYesHTTPS endpoint to deliver events
eventsarrayYesEvent types to subscribe to (["*"] for all)
secretstringNoAuto-generated if omitted; used to sign payloads
descriptionstringNoHuman-readable label
enabledbooleanNoDefault: true

Example

curl -X POST https://api.allroutes.ai/v1/webhooks \
  -H "Authorization: Bearer allroutes_mgmt_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/api/allroutes-webhooks",
    "events": ["budget.threshold", "budget.exhausted", "completion.error"],
    "description": "Production budget and error alerts"
  }'

Response

{
  "id": "whk_abc123",
  "url": "https://your-app.com/api/allroutes-webhooks",
  "events": ["budget.threshold", "budget.exhausted", "completion.error"],
  "secret": "whsec_a1b2c3d4...",
  "enabled": true,
  "created_at": "2026-04-05T10:00:00Z"
}

Save the secret -- it's required to verify incoming payloads and is only returned at creation time.

Payload Format

Every webhook delivery is an HTTPS POST with a JSON body and a few specific headers:

POST /api/allroutes-webhooks HTTP/1.1
Content-Type: application/json
AllRoutes-Webhook-Id: evt_xyz789
AllRoutes-Webhook-Event: budget.threshold
AllRoutes-Webhook-Timestamp: 1714000000
AllRoutes-Webhook-Signature: t=1714000000,v1=5257a869...
{
  "id": "evt_xyz789",
  "type": "budget.threshold",
  "created_at": "2026-04-05T14:30:00Z",
  "data": {
    "key_id": "key_abc123",
    "key_name": "Production Backend",
    "threshold_percent": 80,
    "period": "monthly",
    "budget_usd": 1000.00,
    "spent_usd": 800.43,
    "period_end": "2026-05-01T00:00:00Z"
  }
}

Verifying Signatures

The AllRoutes-Webhook-Signature header is HMAC-SHA256 over <timestamp>.<body> using your webhook secret. Verify before trusting the payload.

Node.js

import crypto from "crypto";

function verifyWebhook(payload: string, headers: Record<string, string>, secret: string): boolean {
  const sig = headers["allroutes-webhook-signature"];
  const ts = headers["allroutes-webhook-timestamp"];
  if (!sig || !ts) return false;

  // Reject events older than 5 minutes (replay protection)
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${ts}.${payload}`)
    .digest("hex");

  // Parse "t=...,v1=<hex>"
  const v1 = sig.split(",").find((p) => p.startsWith("v1="))?.slice(3);
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1 ?? ""));
}

Python

import hmac
import hashlib
import time

def verify_webhook(payload: bytes, headers: dict, secret: str) -> bool:
    sig = headers.get("allroutes-webhook-signature", "")
    ts = headers.get("allroutes-webhook-timestamp", "")
    if not sig or not ts:
        return False

    # Reject events older than 5 minutes
    if abs(time.time() - int(ts)) > 300:
        return False

    expected = hmac.new(
        secret.encode(),
        f"{ts}.".encode() + payload,
        hashlib.sha256,
    ).hexdigest()

    v1 = next((p[3:] for p in sig.split(",") if p.startswith("v1=")), "")
    return hmac.compare_digest(expected, v1)

Delivery and Retries

  • Timeout -- 10 seconds per delivery attempt
  • Success -- any 2xx response
  • Retries -- exponential backoff (10s, 1m, 10m, 1h, 6h, 24h) on 5xx, timeout, or connection errors. After 6 failed attempts (3 days total), the event is dropped and the webhook is auto-disabled
  • Concurrency -- events for the same webhook are delivered in order; events for different webhooks are parallel

A 4xx response (other than 408/429) is treated as a permanent failure and not retried -- this lets you intentionally reject events you don't care about.

Idempotency

Every event has a stable id (e.g., evt_xyz789). The same event may be delivered more than once if your endpoint times out and the retry succeeds. Deduplicate using id on receipt.

Testing

Trigger a test event from the dashboard, or via the API:

curl -X POST https://api.allroutes.ai/v1/webhooks/whk_abc123/test \
  -H "Authorization: Bearer allroutes_mgmt_..." \
  -d '{"type": "budget.threshold"}'

The test sends a synthetic payload with data.test: true so you can wire up your handler without consuming real budget.

Managing Subscriptions

# List
curl https://api.allroutes.ai/v1/webhooks -H "Authorization: Bearer allroutes_mgmt_..."

# Update
curl -X PATCH https://api.allroutes.ai/v1/webhooks/whk_abc123 \
  -H "Authorization: Bearer allroutes_mgmt_..." \
  -d '{"events": ["completion.error"]}'

# Disable
curl -X PATCH https://api.allroutes.ai/v1/webhooks/whk_abc123 \
  -H "Authorization: Bearer allroutes_mgmt_..." \
  -d '{"enabled": false}'

# Delete
curl -X DELETE https://api.allroutes.ai/v1/webhooks/whk_abc123 \
  -H "Authorization: Bearer allroutes_mgmt_..."

Troubleshooting

SymptomCauseFix
Webhook auto-disabled6 consecutive failuresRe-enable in dashboard, fix endpoint, send test event
Signature verification failsWrong secret or body parsed before hashingHash the raw bytes, not the parsed JSON
Events out of orderEndpoint returns slow on some eventsUse a queue between webhook receiver and processing logic
Test event has no effectTest events have data.test: trueFilter test events in your handler if you don't want them

See Also

  • Broadcast -- continuous high-throughput tracing
  • Analytics -- the queryable backing store for event metrics
  • Guardrails -- subscribe to guardrail.triggered for security events
  • Fine-Tuning -- pair with fine_tuning.job.succeeded for pipelines