# Webhooks

> Register HTTP endpoints that receive real-time email events.

Source: https://sndr.sh/docs/cranl/webhooks

Webhooks POST a JSON event to your HTTPS endpoint when an email’s status changes. Respond with a 2xx within 10 seconds; other responses are retried, up to 12 attempts in total. Every request is signed with HMAC-SHA256 so you can verify it came from SNDR.

## Signature verification

Each request carries X-Sndr-Signature: t=<unix seconds>,v1=<hex>. v1 is the HMAC-SHA256 of "<t>.<raw body>", keyed with the webhook secret. While a staged key exists, a second signature v1s is added; accept the request if any signature matches.

```js
const crypto = require('crypto')

// header  = req.headers['x-sndr-signature']
// rawBody = raw request body as a string, before JSON.parse
function verifySignature(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSec) return false
  const expected = crypto.createHmac('sha256', secret)
    .update(parts.t + '.' + rawBody)
    .digest('hex')
  return Object.entries(parts).some(([key, sig]) =>
    key.startsWith('v1') &&
    sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)))
}
```

## Event payload

```json
{
  "id":         "evt_5c1e9a7b-2d4",
  "type":       "email.delivered",
  "created_at": "2026-06-01T12:00:04Z",
  "data": {
    "email_id": "em_3f9c1a2b-7d4",
    "to":       ["customer@example.com"]
  }
}
```

email.bounced and email.failed events carry data.email_id and data.error instead of data.to.

## Supported events

- email.delivered — the receiving mail server accepted the email.
- email.bounced — the recipient address was rejected as invalid (hard bounce).
- email.failed — delivery failed for another reason.
- email.unsubscribed — a recipient unsubscribed using the link in an email.
- email.queued and email.complained — accepted when subscribing, but not currently sent.

- [List webhooks](https://sndr.sh/docs/cranl/webhooks/list-webhooks) `GET /api/cranl/v1/webhooks`: List all webhooks on the connected account.
- [Create webhook](https://sndr.sh/docs/cranl/webhooks/create-webhook) `POST /api/cranl/v1/webhooks`: Register a webhook.
- [Delete webhook](https://sndr.sh/docs/cranl/webhooks/delete-webhook) `DELETE /api/cranl/v1/webhooks/{id}`: Delete a webhook.
- [List signing keys](https://sndr.sh/docs/cranl/webhooks/list-signing-keys) `GET /api/cranl/v1/webhooks/{id}/signing-keys`: List the webhook’s rotation keys (status primary, secondary or retired) without secrets.
- [Stage signing key](https://sndr.sh/docs/cranl/webhooks/stage-signing-key) `POST /api/cranl/v1/webhooks/{id}/signing-keys/stage`: Create a secondary key and return its secret once.
- [Promote signing key](https://sndr.sh/docs/cranl/webhooks/promote-signing-key) `POST /api/cranl/v1/webhooks/{id}/signing-keys/{key_id}/promote`: Make a secondary key the primary.
- [Retire signing key](https://sndr.sh/docs/cranl/webhooks/retire-signing-key) `POST /api/cranl/v1/webhooks/{id}/signing-keys/{key_id}/retire`: Retire a secondary key without promoting it.
