Stripe: webhook signature verification failing [2026 fix]
Webhook signature mismatch—verify endpoint secret, check raw body usage, ensure no JSON parsing before validation.
Stripe: Webhook Signature Verification Failing [2AM Emergency Fix]
TL;DR
Cause: Your code is validating the signature AFTER parsing the request body to JSON, or using the wrong endpoint secret. Fix: Always validate the raw request body BEFORE parsing, and confirm your endpoint secret matches the webhook endpoint in Stripe Dashboard.---
Real Console Error Messages
Here are exact errors you'll see when this breaks:
``` Error: No signatures found matching the expected signature for payload. ```
``` StripeSignatureVerificationError: Unable to verify webhook signature Expected sig: t=1234567890,v1=abc123... ```
``` Error: Webhook Error: Invalid signature. Webhook secret does not match expected value ```
``` TypeError: Cannot read property 'id' of undefined at verifyWebhookSignature ```
``` StripeInvalidSignatureError: No signatures found matching the expected signature for payload. Received signature(s): [] ```
---
The Broken vs. Fixed Code
❌ BROKEN: Parsing Before Validation
```javascript
// Express.js example - WRONG
app.post('/webhook', express.json(), (req, res) => {
// ⚠️ PROBLEM: Body is already parsed to JSON object
const sig = req.headers['stripe-signature'];
try {
const event = stripe.webhooks.constructEvent(
req.body, // ❌ This is an object, not raw string
sig,
process.env.STRIPE_ENDPOINT_SECRET
);
res.json({ received: true });
} catch (err) {
res.status(400).send(Webhook Error: ${err.message});
}
});
```
✅ FIXED: Validate Raw Body First
```javascript // Express.js example - CORRECT const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post(
'/webhook',
express.raw({ type: 'application/json' }), // ✅ Keep as Buffer
(req, res) => {
const sig = req.headers['stripe-signature'];
try {
const event = stripe.webhooks.constructEvent(
req.body, // ✅ Now a Buffer/raw string
sig,
process.env.STRIPE_ENDPOINT_SECRET // ✅ Verify this matches Dashboard
);
// Only parse AFTER validation
console.log('Verified event:', event.type);
res.json({ received: true });
} catch (err) {
console.error('Webhook error:', err.message);
res.status(400).send(Webhook Error: ${err.message});
}
}
);
```
❌ BROKEN: Wrong Endpoint Secret
```javascript const event = stripe.webhooks.constructEvent( req.body, sig, 'whsec_live_wrong1234567890' // ❌ Copy-pasted from old endpoint ); ```
✅ FIXED: Correct Secret from Environment
```javascript const event = stripe.webhooks.constructEvent( req.body, sig, process.env.STRIPE_ENDPOINT_SECRET // ✅ whsec_live_abc123... ); ```
---
Critical Checklist
1. Middleware Order Matters: If using Express, place webhook route BEFORE express.json() middleware:
```javascript
app.post('/webhook', express.raw({type: 'application/json'}), handler);
app.use(express.json()); // Other routes after
```
2. Endpoint Secret Match:
- Go to Stripe Dashboard → Developers → Webhooks
- Click your endpoint (e.g., https://yourdomain.com/webhook)
- Copy the Signing secret (starts with whsec_)
- Verify it matches process.env.STRIPE_ENDPOINT_SECRET
3. Environment Variable Typo: ```bash # Check exact name - case sensitive echo $STRIPE_ENDPOINT_SECRET ```
---
Still Broken? Check These Too
1. Reverse Proxy or Load Balancer: If behind nginx/AWS ALB, request body might be re-encoded. Check raw logs with req.rawBody or use middleware like bodyParser.raw() with proper configuration.
2. Multiple Webhook Endpoints: Stripe Dashboard shows all endpoints. You might be testing against a test mode endpoint while using your live secret—or vice versa. Confirm webhook is active (green checkmark) and matches your URL exactly.
3. Timestamp Validation: Stripe includes a timestamp in the signature (tolerance ±5 minutes). If server time is severely skewed, validation fails. Run date on production and sync NTP if off by >5 minutes.
---
Framework-Specific Notes
Next.js API Routes: Use req.body as Buffer if configured correctly, but most next defaults parse JSON. Solution: [webhook-signature-verification guide](/?guide=nextjs-stripe).
FastAPI/Python: Ensure you read the raw body before parsing: raw_body = await request.body() then pass to stripe.Webhook.construct_event(raw_body, sig_header, secret).
Uncertainty Flag: Version-specific behavior varies significantly between stripe-node v8 vs v10+. If using stripe@^10, the constructEvent() method has different buffer handling than v8. Check your package.json and test locally.
---
Resources
---
Found a different variation? Drop it in the comments — webhook issues are environment-specific and your edge case helps others at 2am.