# Webhooks

A webhook endpoint (`POST /webhooks`) receives the public events of your account (the same ones returned by `GET /events`) as HTTPS `POST` requests, one event per request, signed with a signing secret specific to that endpoint. Delivery is at least once, so your receiver must be idempotent on the event `id`. Events are not guaranteed to arrive in order.

For every field and parameter, see the [Webhooks reference](https://cademi.dev/api/reference/webhooks.md). For the event catalog, see [Events](https://cademi.dev/api/events.md).

## Registering an endpoint

- `url` must use `https` and point to a public host on port 443, 80, or 8443. Private IP addresses, loopback, link-local, and metadata addresses, as well as any other port, are rejected both when you register the endpoint and when a delivery is sent. `http://localhost` is accepted only in sandbox accounts.
- `event_types[]` lists the event types to subscribe to, taken from the catalog documented by `GET /events`. `resource_filters` optionally narrows deliveries by resource type and IDs. `description` is free text.
- The create response includes the `secret` (`whsec_…`) **only once**. It cannot be retrieved afterwards. `POST /webhooks/{webhook_id}/secret-rotations` generates a new signing secret and keeps the previous one valid for `overlap_hours` (24 by default, up to 72). During that period, each delivery carries one signature per active secret.
- `status` is `active` or `inactive`. Changing it with `PATCH` requires the `webhooks.activate` permission. `DELETE` cancels pending deliveries; the delivery history is kept for the retention period.

## Payload and headers

Body: `{"id": "evt_…", "type": "…", "version": 1, "occurred_at": "…", "data": {...}, "delivery_id": "whd_…", "attempt": n}`.

Headers: `Cademi-Signature: t=<unix>,v1=<hex>[,v1=<hex>]`, `Cademi-Signature-Version: 1`, `Cademi-Webhook-Id`, `Cademi-Delivery-Id`, `Cademi-Event-Type`, `User-Agent: Cademi-Webhooks/3`, `Content-Type: application/json`.

## Verifying the signature

```
t        = value of "t" in Cademi-Signature
expected = HMAC_SHA256(secret, t + "." + raw_body)   # lowercase hex
valid    = any "v1" in the header == expected (constant-time comparison)
           and |now - t| <= 300 seconds
```

Use the raw body exactly as received, without re-serializing the JSON. During a secret rotation, check every `v1` in the header against the secret you hold; a single match is enough.

## Responses and retries

| Endpoint response | Result |
|---|---|
| `2xx` | Delivery becomes `delivered` |
| `429` or `5xx`, timeout, network error | New attempt with exponential backoff and jitter (capped at 6 hours; `Retry-After` is honored up to 6 hours) |
| Any other `4xx` | Delivery becomes `failed` (not retried) |
| Destination rejected by the network policy | Delivery becomes `failed`, and the attempt records a `transport_error` |
| 10 attempts or 72 hours | Delivery becomes `dead` |

The connection timeout is 5 seconds and the response timeout is 10 seconds. Up to 64 KiB of the response is read, and a masked excerpt (`response_excerpt`, up to 4 KiB) is stored for troubleshooting. Redirects are not followed.

## Inspecting and resending deliveries

- `GET /webhooks/{webhook_id}/deliveries`, `GET .../deliveries/{webhook_delivery_id}`, `GET .../attempts`, and `GET /webhooks/deliveries` (across all webhook endpoints accessible with the current credentials) require the `webhooks.deliveries.read` permission.
- `POST .../deliveries/{webhook_delivery_id}/attempts` resends a delivery manually (same event `id`, new `attempt` number) and requires the `webhooks.replay` permission.
- `POST /webhooks/{webhook_id}/replays` creates a `webhook.replay` operation over a set of retained events (selected by IDs, time range, or event types). Track the per-event results with `GET /operations/{operation_id}`. See [Operations](https://cademi.dev/api/operations.md).
- `GET /webhooks/{webhook_id}` includes `health` for the last 24 hours: delivered, failed, dead, and pending counts, the last success and failure, and consecutive failures.

## Retention and sandbox

Events and deliveries are retained for 30 days, and attempts are retained with their deliveries. An event is removed only after its deliveries. In a sandbox account, deliveries to webhook endpoints registered in that sandbox are real, so you can use them as your test receiver. See [Sandbox](https://cademi.dev/api/sandbox.md).
