Stripe: webhook signature verification failing [2026 fix]
Webhook signature mismatch due to raw body not preserved or wrong signing secret. Ensure raw request body is passed to signature verification, not parsed JSON.
Stripe: Webhook Signature Verification Failing [2026 Fix]
TL;DR
Cause: Your framework is parsing the request body before Stripe's signature verification runs, destroying the raw bytes needed for cryptographic validation. Fix: Pass the raw unparsed request body tostripe.Webhook.constructEvent() instead of parsed JSON.---
Real Console Error Messages
``` Error: No signatures found matching the expected signature for payload. Managed signing secret mismatch. ```
``` StripeSignatureVerificationError: No signatures found matching the expected signature for payload. Possible reasons: (1) webhook secret is incorrect, (2) payload was not sent as received ```
``` Error: Invalid signature: sig_xxx does not match computed signature. Webhook endpoint received event but verification failed at timestamp validation. ```
``` TypeError: Cannot read property 'rawBody' of undefined at verifyWebhookSignature (/app/webhooks.js:42:15) ```
``` Stripe webhook error: Signature verification failed. Computed sig=sha256=abc... but received sig=sha256=xyz... Payload mismatch detected. ```
---
The Problem: Side-by-Side Code Fix
❌ BROKEN CODE (Express.js)
```javascript const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const app = express();
// WRONG: Body parser runs BEFORE webhook handler app.use(express.json());
app.post('/webhooks/stripe', (req, res) => {
const sig = req.headers['stripe-signature'];
try {
// BROKEN: req.body is already parsed JSON, raw bytes lost
const event = stripe.webhooks.constructEvent(
req.body, // ← This is a JavaScript object, not raw bytes
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
res.json({ received: true });
} catch (err) {
res.status(400).send(Webhook Error: ${err.message});
}
});
```
✅ FIXED CODE (Express.js)
```javascript const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const app = express();
// CORRECT: Raw body middleware runs FIRST, ONLY for Stripe webhooks
app.post(
'/webhooks/stripe',
express.raw({ type: 'application/json' }), // ← Raw body, not parsed
(req, res) => {
const sig = req.headers['stripe-signature'];
try {
// FIXED: req.body is raw Buffer, signature verification works
const event = stripe.webhooks.constructEvent(
req.body, // ← This is Buffer with original bytes
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
// Handle event
if (event.type === 'payment_intent.succeeded') {
console.log('Payment succeeded:', event.data.object.id);
}
res.json({ received: true });
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
res.status(400).send(Webhook Error: ${err.message});
}
}
);
// Parse JSON for OTHER routes app.use(express.json()); ```
❌ BROKEN CODE (Next.js API Route)
```javascript // pages/api/webhooks/stripe.js - BROKEN export default async (req, res) => { const sig = req.headers['stripe-signature']; const body = JSON.stringify(req.body); // ← Re-stringifying loses original try { const event = stripe.webhooks.constructEvent( body, sig, process.env.STRIPE_WEBHOOK_SECRET ); } catch (err) { return res.status(400).json({ error: err.message }); } }; ```
✅ FIXED CODE (Next.js API Route)
```javascript // pages/api/webhooks/stripe.js - FIXED export const config = { api: { bodyParser: false, // ← Disable automatic body parsing }, };
export default async (req, res) => { const sig = req.headers['stripe-signature']; // Read raw body as Buffer const chunks = []; for await (const chunk of req) { chunks.push(chunk); } const rawBody = Buffer.concat(chunks); try { const event = stripe.webhooks.constructEvent( rawBody, // ← Original bytes preserved sig, process.env.STRIPE_WEBHOOK_SECRET ); res.status(200).json({ received: true }); } catch (err) { res.status(400).json({ error: err.message }); } }; ```
---
Why This Happens
Stripe signs the exact raw request body bytes. When your framework automatically parses JSON:
1. Original bytes → parsed JavaScript object 2. Object → re-serialized (different whitespace/order) 3. Signature hash = different result 4. Verification fails
The cryptographic signature is only valid for the exact bytes Stripe sent. Any transformation breaks it.
---
Still Broken? Check These Too
1. Wrong webhook secret: Verify STRIPE_WEBHOOK_SECRET in .env matches Stripe Dashboard → Developers → Webhooks. Copy the entire whsec_xxx string, not your signing secret.
2. Multiple body parsers: If using app.use(express.json()) globally, it runs before route handlers. Move it AFTER webhook routes or create a separate app instance. See [debugging middleware order](/?guide=express-middleware-order) for details.
3. Running old Stripe SDK: stripe npm package < v8 has different API. Verify version: npm list stripe. Update with npm install stripe@latest. Version behavior differs; we're testing against v14+ but cannot guarantee older versions.
---
Testing Your Fix
```bash
Use Stripe CLI to send test event
stripe listen --forward-to localhost:3000/webhooks/stripe stripe trigger payment_intent.succeeded ```You should see: Webhook signature verification failed: No signatures found... → disappeared after applying fix.
---
Related Resources
---
Found a different variation? Drop it in the comments.