BugForge
All lessons
Node.js · 10/19 6 min

Webhooks and signatures

HMAC over the raw body, timing-safe compare, replay windows, and answering 2xx fast.

A webhook is someone else's server calling yours. You must prove the call is genuine, process it once, and answer quickly.

Verify the signature

const crypto = require("node:crypto");

function verify(rawBody, header, secret) {
  const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(header ?? "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
  • Sign the raw bytes. JSON.stringify(req.body) after express.json() may differ in spacing or key order, so the HMAC won't match. Use express.raw({ type: "application/json" }) on the webhook route.
  • timingSafeEqual throws if the lengths differ: check length first.
  • === on secrets leaks timing; use the safe compare.
  • Bad signature → 401, and return so the handler stops.

Stop replays

Providers send a timestamp, usually in seconds, and often sign timestamp + "." + body.

const age = Math.floor(Date.now() / 1000) - Number(ts);
if (!Number.isFinite(age) || Math.abs(age) > 300) return res.status(400).end();

Process once

Providers retry until they get a 2xx, so the same event can arrive several times. Store the event id; if you've seen it, answer 200 and do nothing.

Answer fast, and with 2xx

  • Unknown event types: 200 and ignore. A 4xx makes the provider retry forever.
  • Do slow work after responding, or queue it.
  • A switch on event.type needs a break/return per case; fall-through runs the next handler too.
Next in Node.js
Caching without stale bugs