# Signatures

Every delivery carries a `Cademi-Signature` header, so your endpoint can confirm that the body came from Cademí and was not changed:

```text
Cademi-Signature: t=1759147200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

`t` is the time of the delivery, in Unix seconds. Each `v1` is an HMAC-SHA256 of `<t>.<body>` with a signing secret of the webhook, in hexadecimal. The secret is in the dashboard (see [Signing secret](https://cademi.dev/webhooks/dashboard.md#signing-secret)).

## Verify

1. Read `t` and every `v1` from the header.
2. Compute `HMAC-SHA256(secret, t + "." + body)` over the raw body, exactly as received, before any JSON parsing.
3. Accept the delivery if your result matches any `v1`, using a constant-time comparison.
4. Reject deliveries whose `t` is more than 5 minutes away from your clock.

```js
import crypto from 'node:crypto';

function verify(header, body, secret) {
  const parts = header.split(',').map((p) => p.split('='));
  const t = parts.find(([k]) => k === 't')?.[1];
  const signatures = parts.filter(([k]) => k === 'v1').map(([, v]) => v);
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${body}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) <= 300;

  return fresh && signatures.some((s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}
```

While a new secret is replacing the old one, the header carries one `v1` per active secret, the newest first. Checking against any of them keeps your endpoint working during the change.

API webhooks are signed the same way; see [API webhooks](https://cademi.dev/api/webhooks.md#verifying-the-signature).
