NewIntegration and go-live readiness, rebuiltSee what's new
marupay
All guides

Webhooks6 min read

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.

MaruPay team

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

header
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.
verify.ts
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.

Give your Korean users a gift voucher option.

Tell us about your service. We'll set up your console account, walk your team through the integration test and open live payments when you pass.