Webhooks
Signature verification
One header, one HMAC, constant-time compare — the check that stands between your endpoint and spoofed purchases.
With a signing secret set, every delivery carries X-Growth-Signature: the HMAC-SHA256 of the raw request body, hex-encoded, keyed with your secret. Verify against the exact bytes received, before any JSON parsing — a re-serialized body will never match.
import { createHmac, timingSafeEqual } from "node:crypto";
/** rawBody must be the exact bytes received — verify BEFORE JSON.parse. */
function isFromFictura(rawBody: string, signature: string, secret: string): boolean {
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(signature, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}- In Express, capture the raw body with
express.raw()(or theverifycallback) on the webhook route — a globally appliedexpress.json()consumes the bytes first. - The connect-time
"type": "test"delivery is signed too, so you can verify your verifier before anything real flows. - No secret configured → no header. Setting one is optional but strongly recommended: without it, anyone who learns your URL can mint fake purchases.