Stripe: webhook signature verification failing [2026 fix]
Webhook signature verification fails when endpoint secret is missing, misconfigured, or raw body isn't passed to verification function.
Stripe: Webhook Signature Verification Failing [2026 Fix]
TL;DR
Cause: You're passing parsed JSON toverifySignatureHeader() instead of the raw request body string.
Fix: Capture the raw body *before* JSON parsing, then pass that string to the verification function.---
Real Console Error Messages
``` Error: No signatures found matching the expected signature for payload. ```
``` StripeSignatureVerificationError: No signatures found matching the expected signature for payload. Webhook id: evt_XXXXXXXXXXXXX ```
``` UnexpectedError: Webhook signing secret not found in environment ```
``` SignatureVerificationError: Unable to extract timestamp and signatures from header 'stripe-signature' ```
``` TypeError: Cannot read property 'verifySignatureHeader' of undefined ```
---
Broken Code vs. Fixed Code
Problem #1: Parsing Body Before Verification
BROKEN:
```javascript
app.post('/webhook', express.json(), (req, res) => {
const sig = req.headers['stripe-signature'];
const secret = process.env.STRIPE_WEBHOOK_SECRET;
try {
// ❌ WRONG: req.body is already parsed JSON object
const event = stripe.webhooks.constructEvent(
req.body, // This is an object, not a string!
sig,
secret
);
res.json({ received: true });
} catch (err) {
res.status(400).send(Webhook error: ${err.message});
}
});
```
FIXED:
```javascript
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['stripe-signature'];
const secret = process.env.STRIPE_WEBHOOK_SECRET;
try {
// ✅ CORRECT: req.body is raw Buffer/string
const event = stripe.webhooks.constructEvent(
req.body, // Buffer from express.raw()
sig,
secret
);
res.json({ received: true });
} catch (err) {
res.status(400).send(Webhook error: ${err.message});
}
});
```
Problem #2: Missing Environment Variable
BROKEN: ```javascript const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['stripe-signature']; // ❌ WRONG: Secret key used instead of webhook secret const event = stripe.webhooks.constructEvent( req.body, sig, process.env.STRIPE_SECRET_KEY // This is wrong! ); }); ```
FIXED: ```javascript require('dotenv').config();
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
if (!process.env.STRIPE_WEBHOOK_SECRET) { throw new Error('STRIPE_WEBHOOK_SECRET not set in environment'); }
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['stripe-signature']; // ✅ CORRECT: Dedicated webhook secret const event = stripe.webhooks.constructEvent( req.body, sig, process.env.STRIPE_WEBHOOK_SECRET // Different from API key! ); }); ```
Problem #3: Wrong Middleware Stack Order
BROKEN: ```javascript app.use(express.json()); // ❌ Parses all bodies globally app.post('/webhook', (req, res) => { // req.body is already parsed object const event = stripe.webhooks.constructEvent(req.body, sig, secret); }); ```
FIXED: ```javascript app.use(express.json()); // Safe for other routes
// ✅ CORRECT: Raw middleware ONLY for webhook route app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const event = stripe.webhooks.constructEvent(req.body, sig, secret); });
app.post('/api/charges', express.json(), (req, res) => { // This still gets parsed JSON }); ```
---
Verification Checklist
1. Environment Variable Set? ```bash echo $STRIPE_WEBHOOK_SECRET # Must output: whsec_live_XXXX or whsec_test_XXXX ```
2. Endpoint Secret Matches Dashboard?
- Go to: [Stripe Dashboard](https://dashboard.stripe.com/webhooks) → Endpoints
- Click your endpoint → "Signing secret" button
- Verify exact match (including whsec_ prefix)
3. Test Locally? ```bash stripe listen --forward-to localhost:3000/webhook # Outputs: Ready! Your webhook signing secret is: whsec_test_XXXX stripe trigger payment_intent.succeeded ```
---
Still Broken? Check These Too
1. Multiple Webhook Endpoints (Staging vs. Production)
process.env.NODE_ENV and load corresponding secret from .env.production or .env.staging.2. Body Logged Before Middleware
req.body in authentication middleware *before* the webhook route, it consumes the stream. Move logging *after* verification or use req.rawBody capture.3. Stripe Library Version Mismatch
stripe.webhooks.constructEvent(). v11+ uses stripe.webhooks.constructEvent() identically, but ensure your Node.js version matches Stripe SDK requirements. *We're uncertain if v14+ changed this API; check official docs if upgrading.*---
Related Guides
---
Official Resources
---
Found a different variation? Drop it in the comments. 2am debugging is rough—help the next person by sharing your specific error and fix.