Webhook Testing in Node.js
Set up a webhook receiver, verify HMAC signatures, and debug payloads — complete Node.js walkthrough from first request to production-ready handler.
TL;DR: Use Express (or the built-in http module) to receive webhook POST requests. Always use raw body middleware for HMAC signature verification — never parse JSON before you verify the signature.
Minimal Webhook Receiver in Node.js
The fastest way to start receiving webhooks in Node.js is with Express. Install it with npm install express, then create a route that accepts POST requests, reads the headers, and returns 200.
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook', (req, res) => {
console.log('Event type:', req.headers['x-event-type']);
console.log('Payload:', req.body);
res.status(200).json({ received: true });
});
app.listen(3000, () => console.log('Listening on port 3000'));This is enough to accept any JSON webhook. The provider expects a 2xx response within its timeout window (typically 5–30 seconds). Return it immediately — do not wait for processing to complete before responding.
Using Requex During Development
Before you write a single line of handler code, point your webhook provider at a Requex.me endpoint. Requex gives you a public URL instantly — no tunneling, no ngrok, no server required. Trigger the real event in your provider (a test payment, a push event, a new order) and inspect the exact payload structure in Requex.
Once you know the exact shape of the payload and which headers carry the signature, switch the provider URL to your local server and replay the same request using curl:
curl -X POST http://localhost:3000/webhook \
-H "Content-Type: application/json" \
-H "X-Event-Type: payment.succeeded" \
-d '{"id":"evt_123","amount":4900,"currency":"usd"}'Raw Body for HMAC Signature Verification
Most webhook providers (Stripe, GitHub, Shopify) sign their payloads with an HMAC-SHA256 signature. The signature is computed over the raw request body bytes. If you call express.json() before verifying, Express has already parsed and re-serialized the body — the bytes no longer match and your signature check will always fail.
The fix is to mount express.raw() on the specific webhook route instead of the global JSON middleware:
const crypto = require('crypto');
app.post('/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.headers['x-signature-256'];
const secret = process.env.WEBHOOK_SECRET;
const expected = crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex');
if (sig !== `sha256=${expected}`) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Handle event...
res.json({ received: true });
}
);With express.raw(), req.body is a Buffer containing the exact bytes sent by the provider. Always use crypto.timingSafeEqual in production to prevent timing-attack signature forgery.
Async Processing — Respond Fast
Webhook providers measure your response time strictly. If your handler writes to a database, calls a third-party API, or sends an email, those operations can easily exceed the provider's timeout. The standard pattern is to return 200 immediately, then fire the actual work after the response is sent:
app.post('/webhook', express.json(), (req, res) => {
res.json({ received: true }); // Respond immediately
setImmediate(() => processWebhook(req.body)); // Process async
});
async function processWebhook(payload) {
// Safe to do slow work here — response is already sent
await db.insert(payload);
await notifySlack(payload);
}For higher reliability in production, push the payload to a queue (Bull, BullMQ, SQS) instead of using setImmediate. Queues give you retries and dead-letter handling if the background job fails.
Testing Without a Running Server
The most common development workflow is:
- Point the provider at a Requex endpoint to capture the real payload.
- Copy the full request (headers + body) from the Requex inspector.
- Replay it against your local handler with
curlas many times as needed. - Iterate on your handler logic without triggering a real event each time.
This loop is significantly faster than trying to trigger real events and waiting for the provider to deliver each time. It also lets you test edge-case payloads that are hard to trigger naturally (failed payment, account suspended, etc.).
Capture Your First Node.js Webhook
Get a public endpoint in seconds. Inspect headers, body, and signatures — no server required.
Open Requex →Related guides
Start Testing Webhooks Now
Generate your unique URL and test webhooks instantly. Free, no signup.
Open Webhook Tester →