Verify webhook signatures
Anyone who knows your webhook address can send it a request. The signature proves a request came from FastPay and was not changed on the way.
How signing works#
Each request carries a Fastpay-Webhook-Signature header like t=1761644400,v1=3f9a…. t is the time it was signed, in Unix seconds, and v1 is an HMAC-SHA256 of the text <t>.<raw body>, written in hexadecimal and keyed with your endpoint's signing secret.
Verify a request#
- Read the raw body, before parsing it as JSON.
- Split the header into
tandv1. - Compute the HMAC of
t + "." + bodywith your secret. - Compare it with
v1using a constant-time function. - Reject the request if
tis more than five minutes from now.
import crypto from "node:crypto";
// `raw` is the request body exactly as received, before any JSON parsing.
export function verify(raw, header, secret) {
const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!parts.t || !parts.v1 || age > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${raw}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Rotate the secret#
You can rotate an endpoint's secret in the dashboard. The new secret is shown once, and the old one stops working at once, so update your server first.