In Express, middleware order is everything. If express.json() runs before your webhook route, it consumes the raw body and Stripe's signature check fails.
// app.js — register webhook routes BEFORE express.json()
import webhookRouter from './routes/webhooks.js';
// ✓ Webhook routes first — with raw body parser
app.use('/api/webhooks', express.raw({ type: 'application/json' }), webhookRouter);
// ✓ JSON parser after — for all other routes
app.use(express.json());
app.use('/api', otherRoutes);
// routes/webhooks.js
import express from 'express';
import Stripe from 'stripe';
const router = express.Router();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const secret = process.env.STRIPE_WEBHOOK_SECRET;
router.post('/stripe', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['stripe-signature'];
if (!sig) {
return res.status(400).json({ error: 'Missing stripe-signature header' });
}
let event;
try {
// req.body is a Buffer when using express.raw()
event = stripe.webhooks.constructEvent(req.body, sig, secret);
} catch (err) {
return res.status(400).json({ error: \`Webhook Error: \${err.message}\` });
}
switch (event.type) {
case 'checkout.session.completed':
// fulfill order
break;
case 'customer.subscription.deleted':
// cancel subscription
break;
}
res.json({ received: true });
});
export default router;
bodyParser.raw() from the body-parser package. Since Express 4.16+, express.raw() is built in — no separate package needed.# Quick check — log which middleware runs on the webhook route
app.use('/api/webhooks', (req, res, next) => {
console.log('Content-Type:', req.headers['content-type']);
console.log('Body type:', typeof req.body, Buffer.isBuffer(req.body) ? '(Buffer)' : '');
next();
});
If body type shows object instead of Buffer, express.json() ran first. Fix the route registration order.
Generate the complete Express handler with correct middleware order.
Open WebhookFix →