Stripe: webhook signature verification failing [2026 fix]
Webhook signature invalid due to raw body tampering or endpoint secret mismatch—use raw request body and verify secret environment variable.
Stripe: Webhook Signature Verification Failing [2am Fix]
TL;DR
Cause: You're passing parsed JSON instead of the raw request body to Stripe's signature verification, OR yourSTRIPE_WEBHOOK_SECRET is wrong/missing.
Fix: Use the raw unparsed request body and confirm your endpoint secret matches your webhook configuration in the Stripe Dashboard.---
Exact Console Errors You're Seeing
``` 1. Error: No signatures found matching the expected signature for payload 2. Webhook signature verification failed: Invalid signature 3. TypeError: Cannot read property 'id' of undefined at verifySignatureHeader 4. Invalid signature: expected <sig>, got <sig> 5. Webhook endpoint failed: 403 Forbidden (signature mismatch) ```
---
Broken Code vs. Exact Fix
❌ BROKEN (Express.js)
```javascript const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/webhook', express.json(), (req, res) => {
const sig = req.headers['stripe-signature'];
try {
// WRONG: express.json() already parsed the body
const event = stripe.webhooks.constructEvent(
JSON.stringify(req.body), // ❌ Re-stringifying loses original
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
res.json({received: true});
} catch (err) {
res.status(400).send(Webhook Error: ${err.message});
}
});
```
✅ FIXED (Express.js)
```javascript const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// CRITICAL: Raw body middleware BEFORE express.json()
app.post('/webhook',
express.raw({type: 'application/json'}), // ✅ Raw bytes preserved
(req, res) => {
const sig = req.headers['stripe-signature'];
try {
// CORRECT: Pass raw buffer directly
const event = stripe.webhooks.constructEvent(
req.body, // ✅ Raw body, not re-stringified
sig,
process.env.STRIPE_WEBHOOK_SECRET // ✅ Verify it exists
);
console.log('✓ Webhook verified:', event.type);
res.json({received: true});
} catch (err) {
console.error('Webhook Error:', err.message);
res.status(400).send(Webhook Error: ${err.message});
}
}
);
```
❌ BROKEN (Next.js API Routes)
```javascript export default async function handler(req, res) { const sig = req.headers['stripe-signature']; try { const event = stripe.webhooks.constructEvent( JSON.stringify(req.body), // ❌ Already parsed by Next.js sig, process.env.STRIPE_WEBHOOK_SECRET ); } catch (err) { res.status(400).json({error: err.message}); } } ```
✅ FIXED (Next.js API Routes)
```javascript import { buffer } from 'micro';
export const config = { api: { bodyParser: false, // ✅ Disable automatic parsing }, };
export default async function handler(req, res) { const sig = req.headers['stripe-signature']; const buf = await buffer(req); // ✅ Get raw buffer try { const event = stripe.webhooks.constructEvent( buf, // ✅ Raw buffer sig, process.env.STRIPE_WEBHOOK_SECRET ); res.status(200).json({received: true}); } catch (err) { res.status(400).json({error: err.message}); } } ```
---
Critical Checklist
1. Endpoint Secret Match: In Stripe Dashboard → Developers → Webhooks → Click your endpoint → Copy "Signing secret" and paste into .env
2. Raw Body Middleware Order: Stripe verification middleware MUST come BEFORE express.json() or any body parsing
3. Environment Variable Exists: Run echo $STRIPE_WEBHOOK_SECRET in your deployment environment—if empty, restart your app after updating .env
4. Webhook Testing: Use Stripe CLI to test locally:
```bash
stripe listen --forward-to localhost:3000/webhook
stripe trigger payment_intent.succeeded
```
---
Still Broken? Check These Too
1. Timestamp Skew: Stripe rejects signatures older than 5 minutes. If your server clock is wrong, it will fail. Run ntpdate -s time.nist.gov on Linux or sync time on your server.
2. Multiple Webhook Secrets: You have separate secrets for Test vs. Live mode. Verify you're using the correct one for your current environment—test webhooks won't verify with your live secret.
3. Payload Modification in Transit: Proxies, load balancers, or middleware modifying the request body (e.g., logging middleware that reads the stream) will break the signature. Use a raw body capture that doesn't consume the stream twice. See [webhook debugging guide](/?guide=stripe-webhook-logging).
---
Version Note
⚠️ Stripe.js library behavior: We're referencing stripe.webhooks.constructEvent() which works in stripe@8.0.0+. If you're on stripe@6.x, the method is stripe.webhooks.constructEventAsync() and requires await. Confirm your installed version with npm list stripe.
---
Official Resources
---
Found a different variation? Drop it in the comments—especially if you hit this with Flask, Django, or non-Express frameworks.
Related: [Firebase Cloud Functions + Stripe webhooks](/?guide=firebase-stripe) | [Local webhook testing without ngrok](/?guide=stripe-local-testing)