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. For the event catalog, see Events.
Registering an endpoint
urlmust usehttpsand 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://localhostis accepted only in sandbox accounts.event_types[]lists the event types to subscribe to, taken from the catalog documented byGET /events.resource_filtersoptionally narrows deliveries by resource type and IDs.descriptionis free text.- The create response includes the
secret(whsec_…) only once. It cannot be retrieved afterwards.POST /webhooks/{webhook_id}/secret-rotationsgenerates a new signing secret and keeps the previous one valid foroverlap_hours(24 by default, up to 72). During that period, each delivery carries one signature per active secret. statusisactiveorinactive. Changing it withPATCHrequires thewebhooks.activatepermission.DELETEcancels 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 secondsUse 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, andGET /webhooks/deliveries(across all webhook endpoints accessible with the current credentials) require thewebhooks.deliveries.readpermission.POST .../deliveries/{webhook_delivery_id}/attemptsresends a delivery manually (same eventid, newattemptnumber) and requires thewebhooks.replaypermission.POST /webhooks/{webhook_id}/replayscreates awebhook.replayoperation over a set of retained events (selected by IDs, time range, or event types). Track the per-event results withGET /operations/{operation_id}. See Operations.GET /webhooks/{webhook_id}includeshealthfor 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.