1You parsed the body before verifying the signature
Stripe and most other providers sign the exact bytes they sent — including whitespace, key order, and trailing newlines. The moment Express or Flask runs JSON.parse() on the body and your code re-serializes it, the signature no longer matches the bytes you signed. Use express.raw({ type: "application/json" }) on the webhook route, before any json parser. In Flask, read request.get_data(as_text=False), not request.json.
// Express.js — RIGHT
app.post('/webhook',
express.raw({ type: 'application/json' }), // ← raw body, not json
(req, res) => {
const sig = req.headers['stripe-signature']
const event = stripe.webhooks.constructEvent(
req.body, // Buffer of raw bytes
sig,
process.env.STRIPE_WEBHOOK_SECRET
)
res.json({ received: true })
}
)