Node.js · Code recipe
Verify a webhook signature in Node.js
Prove a delivery came from URLpipe before you act on it, in Node.js 18+ with the built-in fetch. Every program on this page runs as it stands — each one was run against a stub of the API before it was published.
By Roger Campos · Last updated: September 2026
TL;DR
To verify a URLpipe webhook in Node.js, compute HMAC-SHA256 over the X-URLpipe-Timestamp value, a dot and the raw request body, keyed with your whsec_ secret. Prefix it with v1= and compare it with crypto.timingSafeEqual against each comma-separated value of X-URLpipe-Signature; reject timestamps more than five minutes off.
Free plan, no credit card. 1,000 credits a month.
Your report_to URL accepts a POST from anyone who learns it. Turn on webhook signing for the project and every delivery carries X-URLpipe-Timestamp and X-URLpipe-Signature, so the receiver can prove the body came from URLpipe, unchanged, in the last five minutes.
The Node.js receiver below does the whole check: it reads the raw body (express.raw() rather than express.json() on the route), recomputes the HMAC, compares it in constant time with crypto.timingSafeEqual, and rejects stale timestamps. To send it a signed test delivery, use the shell script on the cURL page.
Setup
Before you start
Nothing to install: node:http and node:crypto ship with Node. Turn signing on under Settings → Webhook Signing and copy the secret (it starts with whsec_).
export URLPIPE_WEBHOOK_SECRET="whsec_your_signing_secret"
Receiver
A receiver that verifies every delivery
verify() is the part to copy into your app. In Express, mount the route with express.raw({ type: "application/json" }), not express.json(), so req.body is the Buffer that was signed. timingSafeEqual throws on buffers of different lengths, which is why the length check comes first.
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
const SECRET = process.env.URLPIPE_WEBHOOK_SECRET;
const TOLERANCE = 5 * 60; // seconds
// True when the delivery was signed with SECRET in the last five minutes.
function verify(body, timestamp, signatureHeader) {
if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE) {
return false;
}
const hmac = createHmac("sha256", SECRET).update(`${timestamp}.`).update(body);
const expected = Buffer.from(`v1=${hmac.digest("hex")}`);
// One signature normally, two during a secret rotation: accept any match.
return signatureHeader.split(",").some((signature) => {
const candidate = Buffer.from(signature.trim());
return candidate.length === expected.length && timingSafeEqual(candidate, expected);
});
}
createServer((req, res) => {
if (req.method !== "POST" || req.url !== "/webhooks/urlpipe") {
res.writeHead(404).end();
return;
}
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => {
// The raw bytes, exactly as sent.
const body = Buffer.concat(chunks);
const timestamp = req.headers["x-urlpipe-timestamp"] ?? "";
const signature = req.headers["x-urlpipe-signature"] ?? "";
if (!verify(body, timestamp, signature)) {
res.writeHead(401).end();
return;
}
const { token } = JSON.parse(body);
console.log(`Verified delivery for ${token}`);
res.writeHead(200).end();
});
}).listen(Number(process.env.PORT ?? 8000));
Run it: node webhook.mjs
Details
What to know about signed deliveries
- Signing is off until you turn it on under Settings → Webhook Signing; the secret starts with
whsec_. Enabling it is safe at any time — the body does not change — so enable it first and deploy the check after. - Rotating the secret opens a 24-hour window in which every delivery carries two signatures, the new one first. That is why the header is a list and any match is accepted.
- Each attempt is signed with a fresh timestamp, so a retry passes the five-minute check like the first delivery did.
- A delivery your endpoint rejects is retried with backoff, up to six attempts over roughly twenty minutes, and can be resent by hand from the dashboard afterwards.
- Make the handler idempotent on
token: the same result can arrive more than once.
Other languages
Verify a webhook signature in another language
More Node.js: every Node.js recipe · how signing works, in the docs
FAQ
Frequently asked questions
Why verify against the raw body?
Why can the signature header hold more than one value?
What should a receiver answer when the check fails?
Why a five-minute tolerance?
Get a key and run it.
Free plan, no card. Paste your key into URLPIPE_API_KEY and every program on this page runs as it is.