Webhook Delivery

Updated

Webhooks push triggered alert events to your server as they fire. This is ideal for headless integrations, backend processing pipelines, and systems that cannot maintain a persistent WebSocket connection.

Webhook URL is configured once per user in your notification contacts — then enabled per subscription via the webhook channel toggle.

Setting up a webhook

Step 1: Configure your webhook URL in contacts. The server auto-generates an HMAC-SHA256 signing secret:

webhook-setup.ts
import { Chaos } from '@chaoslabs/ai-sdk';
 
const chaos = new Chaos({ apiKey: process.env.CHAOS_API_KEY! });
 
// Set webhook URL (once per user)
const contacts = await chaos.alerts.contacts.updateWebhook(
  'https://your-server.com/webhooks/alerts'
);
 
// Save the auto-generated secret for signature verification
console.log(contacts.webhookSecret);
 
// Test reachability
const test = await chaos.alerts.contacts.testWebhook(
  'https://your-server.com/webhooks/alerts'
);
console.log(test.success); // true
 
// Step 2: Enable webhook on a subscription
const sub = await chaos.alerts.subscriptions.create({
  alert: {
    alert_type: 'price_change',
    asset: 'ETH',
    target_price: 3000,
    condition: 'above',
  },
  channels: { inApp: true, push: false, telegram: false, email: false, webhook: true },
});

Webhook payload

When the alert triggers, Chaos AI sends a POST request to your URL with the triggered alert as a JSON body:

HeaderValue
Content-Typeapplication/json
X-Chaos-SignatureHMAC-SHA256 hex digest (if secret is set)
webhook-payload.json
{
  "id": "ta-60a5a3feae3b",
  "userId": "user-123",
  "subscriptionId": "ua-d7309261",
  "alert": {
    "alert_type": "price_change",
    "asset": "ETH",
    "target_price": 4000,
    "condition": "above"
  },
  "chain": "ethereum",
  "severity": "warning",
  "title": "ETH price above $4,000",
  "summary": "ETH has crossed your $4,000 threshold, currently at $4,125.",
  "evidence": {
    "dataPoints": [
      { "label": "Current Price", "value": "4125.50", "unit": "USD" },
      { "label": "Threshold", "value": "4000", "unit": "USD" }
    ],
    "chartData": null,
    "sourceLinks": null
  },
  "transactionContext": null,
  "readState": "unread",
  "archived": false,
  "triggeredAt": "2026-03-17T14:30:00Z",
  "createdAt": "2026-03-17T14:30:00Z"
}

Signature verification

If you provided a secret in the webhook config, every delivery includes an X-Chaos-Signature header containing the HMAC-SHA256 hex digest of the request body.

Verify the signature on your server to ensure the request is authentic:

verify-signature.ts
import { createHmac } from 'crypto';
 
function verifyWebhookSignature(
  body: string,
  signature: string,
  secret: string
): boolean {
  const expected = createHmac('sha256', secret)
    .update(body)
    .digest('hex');
  return signature === expected;
}

Express example

express-webhook.ts
import express from 'express';
import { createHmac } from 'crypto';
 
const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!;
 
app.post('/webhooks/alerts', express.text({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-chaos-signature'] as string;
  const body = req.body as string;
 
  // Verify HMAC signature
  const expected = createHmac('sha256', WEBHOOK_SECRET)
    .update(body)
    .digest('hex');
 
  if (signature !== expected) {
    console.error('Invalid webhook signature');
    return res.status(401).send('Unauthorized');
  }
 
  const alert = JSON.parse(body);
  console.log(`[${alert.severity}] ${alert.title}`);
  console.log(`  ${alert.summary}`);
 
  res.status(200).send('OK');
});
 
app.listen(3000);
[@portabletext/react] Unknown block type "callout", specify a component for it in the `components.types` prop

Retry policy

Webhook delivery flows through the SQS notification pipeline. Failed deliveries are automatically retried via SQS visibility timeout re-delivery (default: 5 minutes). The webhook request has a 10-second timeout.

Webhook notifications respect the same per-subscription cooldown as email and telegram — if the cooldown window is active, no external channel (email, telegram, webhook) will fire.

Managing webhooks

Webhook URL is managed in your user contacts, not per subscription. Toggle channels.webhook on each subscription to control which alerts deliver via webhook:

manage-webhooks.ts
// Enable webhook on an existing subscription
await chaos.alerts.subscriptions.update('ua-abc123', {
  channels: { inApp: true, push: false, telegram: true, email: true, webhook: true },
});
 
// Disable webhook on a subscription (other channels unaffected)
await chaos.alerts.subscriptions.update('ua-abc123', {
  channels: { inApp: true, push: false, telegram: true, email: true, webhook: false },
});
 
// Update webhook URL
await chaos.alerts.contacts.updateWebhook('https://new-endpoint.com/webhooks/alerts');
 
// Remove webhook entirely
await chaos.alerts.contacts.removeWebhook();

Webhook vs WebSocket

FeatureWebhookWebSocket
Connection modelStateless — server pushes to your URLPersistent — client maintains open connection
LatencySlightly higher (HTTP round-trip)Lower (already connected)
ReliabilityAutomatic retries on failureAuto-reconnect on disconnect
AuthenticationHMAC-SHA256 signatureAPI key in connection
Best forBackend pipelines, serverless functionsDashboards, bots, real-time UIs

You can use both on the same subscription — the alert will be delivered through all configured channels.

Next steps

Was this helpful?