Verify webhook signatures
The exact signing scheme Hannu uses — x-hannu-signature with a timestamped HMAC-SHA256 — and a verification snippet.
Every webhook delivery is signed. Verify the signature before you act on the payload — it proves the delivery came from Hannu and hasn't been tampered with or replayed.
The scheme
Each delivery carries an x-hannu-signature header:
x-hannu-signature: t=1737100000,v1=3a7f… (hex)t— the Unix timestamp when the delivery was signed.v1—HMAC-SHA256(secret, "<t>.<rawBody>"), hex-encoded.
The secret is your endpoint's signing secret (prefixed whsec_), shown once when you create the endpoint in the dashboard. Sign over the exact raw request body — parse it only after verifying.
Verify it
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyHannuWebhook(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // stale / replayed
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 ?? '');
return a.length === b.length && timingSafeEqual(a, b);
}Compute the HMAC over the raw request body, before any JSON parsing or re-serialization. Re-encoding changes bytes and breaks the signature.
Reject replays
The timestamp (t) bounds the delivery in time. Reject anything older than your tolerance (5 minutes is a good default) so a captured delivery can't be replayed later.
Use exactly the header and construction above (x-hannu-signature: t=…,v1=… over t.body). Older references to X-Signature or sha256=… are superseded.