Signatures

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

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).

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.
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.

On this page