> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wardin.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> HMAC-signed HTTPS callbacks for budget, policy, and anomaly events — at-least-once delivery with retries, plus a native Slack payload.

Webhooks push the same governance events that drive alerting on the dashboard into your own systems — PagerDuty, a SIEM, a Slack channel, an internal bot — without polling the API. Every delivery is signed with HMAC-SHA256 so a receiver can prove it actually came from Wardin, and delivery is at-least-once with retries: a slow or briefly-down receiver doesn't silently lose events.

## Event types

| Event                      | Fires when                                                                                                                              |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `budget.threshold_reached` | A budget's spend crosses its configured threshold (default 80%) within the current billing period                                       |
| `policy.violation`         | A gateway policy blocks a request — a guardrail rejection, a model-allowlist 403, etc.                                                  |
| `anomaly.usage_spike`      | The last hour's request volume is at least 3× the trailing 24-hour baseline (minimum 20 requests, to avoid noise on low-volume tenants) |

Each occurrence alerts **once** — a sustained spike or a budget that stays over threshold doesn't refire on every evaluation pass, only on the first crossing (per budget period, per violation, or per clock-hour, depending on the event).

## Creating an endpoint

```bash theme={null}
curl -X POST https://api.wardin.ai/v1/webhook-endpoints \
  -H "Authorization: Bearer wardin_sk_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Slack #alerts",
    "url": "https://hooks.slack.com/services/T00/B00/xxx",
    "type": "slack",
    "events": ["budget.threshold_reached", "policy.violation"]
  }'
# → { "id", "secret": "whsec_...", ... }
```

`type` is required — `generic` or `slack`. The response includes a per-endpoint signing `secret` (`whsec_...`) — store it; it's needed to verify deliveries.

Full endpoint references: [`GET /v1/webhook-endpoints`](/api-reference/webhook-endpoints/list), [`POST /v1/webhook-endpoints`](/api-reference/webhook-endpoints/create), [`PATCH /v1/webhook-endpoints/{id}`](/api-reference/webhook-endpoints/update), [`DELETE /v1/webhook-endpoints/{id}`](/api-reference/webhook-endpoints/delete).

<Note>
  Endpoint URLs are validated at create/update time and again at delivery time (SSRF guard): private/loopback/link-local addresses and non-HTTPS URLs are rejected in production, and 3xx redirects are never followed. This also blocks DNS rebinding — a hostname that resolves to a public address at config time but a private one at delivery time is caught on the re-check.
</Note>

## Delivery format

A `generic` endpoint receives the full event envelope:

```json theme={null}
{
  "id": "delivery-uuid",
  "event": "budget.threshold_reached",
  "created": "2026-07-11T00:00:00.000Z",
  "data": {
    "message": "Budget at 82% ($410.00 of $500.00)",
    "virtual_key_id": "...",
    "spent_usd": 410,
    "budget_usd": 500,
    "threshold_pct": 80
  }
}
```

A `slack` endpoint instead receives the native Slack incoming-webhook shape — `{ "text": "*Wardin · budget.threshold_reached*\n<message>" }` — so it renders directly in the channel with no adapter needed.

Every POST also carries `X-Wardin-Delivery-Id` (the delivery's id — use it to de-duplicate on your side) and `X-Wardin-Event` (the event type). `generic` endpoints additionally carry the signature header described below; Slack's incoming-webhook receiver ignores custom headers, so signed endpoints are `generic` only.

## Verifying the signature

`generic` deliveries carry an `X-Wardin-Signature` header in the form `t=<unix_seconds>,v1=<hex_hmac>` — a timestamp and an HMAC-SHA256 digest of `<timestamp>.<raw_body>`, keyed with your endpoint's `whsec_...` secret. Binding the timestamp into the digest (rather than signing the body alone) lets you reject replayed deliveries outside a tolerance window.

```js theme={null}
const crypto = require('crypto');

function verifyWardinSignature(secret, rawBody, header, toleranceSeconds = 300) {
  const parts = Object.fromEntries(
    header.split(',').map((kv) => kv.split('=').map((s) => s.trim())),
  );
  const timestamp = Number(parts.t);
  if (!Number.isFinite(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  const a = Buffer.from(parts.v1 ?? '');
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

<Warning>
  Compute the HMAC over the **raw, unparsed request body** — re-serializing a parsed JSON object can reorder keys or change whitespace and produce a digest mismatch even for a genuine delivery.
</Warning>

## Delivery guarantees

Delivery is **at-least-once**: an event is persisted as a delivery row before any network call is attempted, so a crash between "event happened" and "HTTP POST sent" can never lose it. A failed attempt (non-2xx, timeout, or transport error) is retried with exponential backoff (capped at one hour) until it either succeeds or exhausts the endpoint's retry budget, at which point it's dead-lettered — the endpoint's status still reflects the last outcome, but that specific delivery stops retrying. Because delivery is at-least-once, not exactly-once, treat `X-Wardin-Delivery-Id` as an idempotency key if your receiver can't tolerate a duplicate.

## Test send

Send a synthetic `webhook.test` event to one endpoint and get an immediate pass/fail result — useful for confirming a new endpoint and secret are wired correctly before relying on a real event:

```bash theme={null}
curl -X POST https://api.wardin.ai/v1/webhook-endpoints/ENDPOINT_ID/test \
  -H "Authorization: Bearer wardin_sk_ADMIN_KEY"
# → { "delivered": true, "error": null }
```

Full endpoint reference: [`POST /v1/webhook-endpoints/{id}/test`](/api-reference/webhook-endpoints/test).

## Where to see it

Console → **GATEWAY → Webhooks** lists every endpoint (with a live delivered/failed status dot), lets you add/toggle/delete endpoints and reveal a secret, and includes the same test-send action as the API.
