Express.js Webhook Handler
Set up raw body middleware, verify HMAC signatures, and process webhooks asynchronously in Express — from first handler to production pattern.
TL;DR: Mount express.raw() on the webhook path to capture the raw body before JSON parsing. This is required for HMAC verification with Stripe, GitHub, Shopify, and most other providers.
The Raw Body Problem
The most common Express webhook mistake is applying app.use(express.json()) globally and then trying to verify an HMAC signature. By the time your route handler runs, Express has already consumed the raw byte stream and replaced req.body with a parsed JavaScript object. Re-serialising that object to a string gives different bytes than the original — the HMAC will never match.
The solution is to skip the global JSON middleware for webhook routes and instead mount express.raw() at the route level. This makes req.body a Buffer containing the exact bytes the provider sent.
Setting Up Raw Body Middleware
Apply the global JSON parser to all other routes, then override with express.raw() specifically on your webhook path. A small inline middleware promotes the raw buffer onto req.rawBody for use inside the signature verifier:
const express = require('express');
const app = express();
// Global JSON parsing for all other routes
app.use(express.json());
// Webhook route needs RAW body — override with specific middleware
app.post('/webhook',
express.raw({ type: '*/*' }),
(req, res, next) => {
req.rawBody = req.body;
req.body = JSON.parse(req.body.toString());
next();
},
handleWebhook
);After this middleware chain runs, req.rawBody holds the original Buffer and req.body is the parsed object — exactly like a normal JSON route, but with the raw bytes preserved for signature checking.
Generic HMAC Verification Middleware
Rather than repeating the HMAC logic in every route, extract it into a reusable middleware factory. The factory accepts the name of the environment variable holding the secret, the header to read the signature from, and an optional prefix:
const crypto = require('crypto');
function verifyHmac(secretEnvKey, signatureHeader, prefix = 'sha256=') {
return (req, res, next) => {
const sig = req.headers[signatureHeader] ?? '';
const secret = process.env[secretEnvKey];
const hmac = crypto.createHmac('sha256', secret)
.update(req.rawBody)
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(sig),
Buffer.from(`${prefix}${hmac}`)
)) {
return res.status(401).json({ error: 'Invalid signature' });
}
next();
};
}
// Capture raw body into req.rawBody
function captureRawBody(req, res, next) {
req.rawBody = req.body;
req.body = JSON.parse(req.body.toString());
next();
}
app.post('/stripe-webhook',
express.raw({ type: 'application/json' }),
captureRawBody,
verifyHmac('STRIPE_WEBHOOK_SECRET', 'stripe-signature', 't='),
handleStripeWebhook
);
app.post('/github-webhook',
express.raw({ type: 'application/json' }),
captureRawBody,
verifyHmac('GITHUB_WEBHOOK_SECRET', 'x-hub-signature-256'),
handleGithubWebhook
);Note: Stripe's signature header uses a t=timestamp,v1=hash format rather than a plain sha256= prefix. For Stripe specifically, use the official stripe.webhooks.constructEvent() SDK method rather than a generic verifier — it handles the timestamp tolerance check as well.
Responding Quickly and Processing Async
The handler function itself should do as little as possible before sending the 200. Validate the signature, queue the work, return:
function handleWebhook(req, res) {
// Signature already verified by middleware
res.json({ received: true }); // Respond immediately
// Fire-and-forget (use a queue in production)
setImmediate(async () => {
try {
await processEvent(req.body);
} catch (err) {
console.error('Webhook processing error:', err);
}
});
}For production applications, replace setImmediate with a job queue (BullMQ, RabbitMQ, SQS). A queue survives process restarts, gives you retry logic, and provides a dead-letter queue for events that repeatedly fail.
Using Requex to Test Your Handler
The fastest development loop for Express webhook handlers:
- Point the provider at a Requex.me endpoint to capture the real payload and headers.
- Note the exact signature header name (e.g.
x-hub-signature-256) and the prefix format. - Replay the captured payload with
curlagainstlocalhost:3000/webhook. - Iterate on your middleware stack without triggering a real event each time.
# Replay a captured payload against your local Express server
curl -X POST http://localhost:3000/webhook \
-H "Content-Type: application/json" \
-H "X-Hub-Signature-256: sha256=abc123..." \
-d '{"action":"opened","number":42}'Capture the Real Payload First
Inspect headers, signature format, and body structure before writing middleware — no local server needed.
Open Requex →Related guides
Start Testing Webhooks Now
Generate your unique URL and test webhooks instantly. Free, no signup.
Open Webhook Tester →