Verify a webhook signature in Node.js
Validate an HMAC-SHA256 webhook against the raw request body with a constant-time comparison and replay window.
- JavaScript
- GreenSnippets
- MIT
- Executed with Node.js 22
import crypto from "node:crypto";
function verifyWebhook({ rawBody, timestamp, signature, secret, toleranceSeconds = 300 }) {
const sentAt = Number(timestamp);
if (!Number.isFinite(sentAt)) return false;
if (Math.abs(Date.now() / 1000 - sentAt) > toleranceSeconds) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest();
let given;
try {
given = Buffer.from(signature, "hex");
} catch {
return false;
}
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}How it works
Webhook endpoints are public URLs, so anyone can post to them. Verify that the sender knows the shared secret:
- Use the raw body bytes. Parsing then re-serializing JSON changes the bytes and breaks the signature.
- Compute
HMAC-SHA256(secret, timestamp + "." + body). - Compare with
crypto.timingSafeEqual(lengths must match first, or it throws). - Reject timestamps outside a short window to limit replays, and store processed event IDs to make handling idempotent.
The timestamp.body format is a common convention; always follow your provider's documented scheme.
Related snippets
- PHP Tested
Retry a callable with exponential backoff in PHP
Re-run flaky operations with growing delays and jitter, only for chosen exception types.
- TypeScript Tested
Typed fetch helper that checks the response
A fetch wrapper that throws on HTTP errors and validates the JSON body with a type guard.
- PHP Tested
Decode JSON safely in PHP
Parse JSON with exceptions on error and a depth limit, instead of silently getting null.
- JavaScript Tested
Retry an async operation with exponential backoff
Retry a failing promise-returning function with growing, jittered delays and a retry predicate.

