Verifying MaruPay webhook signatures
How the MaruPay-Signature header is built, how to check it against the raw body and the mistakes we see most often.
Every webhook MaruPay sends carries a signature. Checking it is the one thing your handler must do before it credits an order: it proves the event came from us and that nobody changed it on the way.
What the header contains
The MaruPay-Signature header has two parts: t, the Unix time the event was signed, and v1, a hex HMAC-SHA256 of the string t + "." + the raw request body, keyed with your endpoint's signing secret (whsec_…).
MaruPay-Signature: t=1790656319,v1=e2c97b…0f5a
Checking it
- Read the raw body before any JSON parser touches it. Re-serialising parsed JSON changes whitespace and key order, and the signature won't match.
- Compute the HMAC-SHA256 of t + "." + rawBody with your signing secret.
- Compare with a constant-time function such as crypto.timingSafeEqual.
- Reject timestamps more than five minutes from your clock, so a captured request can't be replayed later.
import crypto from "node:crypto";
// header: "t=<unix>,v1=<hex>" body: the raw request body
export function verify(body: string, header: string, secret: string) {
const [, t, v1] = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header) ?? [];
if (!t || !v1) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${body}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(t)) <= 300;
return fresh && crypto.timingSafeEqual(
Buffer.from(v1, "hex"),
Buffer.from(expected, "hex"),
);
}
// Retried deliveries reuse the event ID: credit each one once.
if (await processed.has(event.id)) return res.status(200).end();
After the signature
A valid signature tells you the event is ours, not that you haven't seen it before. Deliveries are retried when your endpoint doesn't answer 2xx, and you can redeliver any event from the console, so the same event can arrive more than once. Store the event ID with the order change in one transaction and return 200 for IDs you've already applied.
Rotating the secret
Create a new signing secret in the console. For 24 hours both secrets sign every event, so you can deploy the new one and then revoke the old one without failing a delivery.