Node.js cookbook · Intermediate
Verify webhook signatures in Express
Accept signed webhooks safely: verify the raw body, prevent replays and drop duplicates.
Approach
Mount express.raw({ type: "application/json" }) on the webhook route only (the HMAC must be computed over the exact raw bytes), verify with crypto.timingSafeEqual, check timestamp tolerance, and insert the event ID with a unique constraint before acknowledging.
Solution
import crypto from "node:crypto";
import express from "express";
const SECRET = process.env.WEBHOOK_SECRET!;
function verify(raw: Buffer, header: string | undefined) {
const parts = Object.fromEntries((header ?? "").split(",").map((p) => p.split("=", 2)));
const ts = Number(parts.t);
const expected = crypto.createHmac("sha256", SECRET).update(`${ts}.`).update(raw).digest();
const given = Buffer.from(parts.v1 ?? "", "hex");
const fresh = Math.abs(Date.now() / 1000 - ts) <= 300;
return fresh && given.length === expected.length && crypto.timingSafeEqual(given, expected);
}
app.post("/webhooks/payments", express.raw({ type: "application/json", limit: "1mb" }), async (req, res) => {
if (!verify(req.body as Buffer, req.get("x-signature"))) return res.status(400).send("Invalid signature");
const event = JSON.parse((req.body as Buffer).toString("utf8"));
const { rowCount } = await pool.query(
"INSERT INTO webhook_events (event_id, type, payload) VALUES ($1, $2, $3) ON CONFLICT (event_id) DO NOTHING",
[event.id, event.type, event],
);
if (rowCount) await webhookQueue.add("process", { eventId: event.id });
res.status(200).json({ received: true });
});
Pitfalls
- Global
express.json()consuming the body before the webhook route. timingSafeEqualthrows on different lengths — check length first.- Processing synchronously and timing out.
Variations
- Provider SDKs (e.g.,
stripe.webhooks.constructEvent).
Further reading: Node.js: crypto.timingSafeEqual
Reference: https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b
More Node.js recipes
- Add health and readiness endpoints to a Node.js service
- Add JWT authentication to an Express API
- Cache expensive lookups in Redis from Node.js
- Call external APIs with timeouts and retries in Node.js
- Centralize error handling in Express 5
- Hash passwords and implement login safely in Node.js
- Implement cursor pagination with PostgreSQL in Node.js
- Offload CPU-heavy work with worker threads