Stripe: webhook signature verification failing [2026 fix]
Webhook signature mismatch from incorrect endpoint secret or missing raw body. Verify STRIPE_WEBHOOK_SECRET matches dashboard and pass raw request body, not parsed JSON.
Stripe Webhook Signature Verification Failing
TL;DR
Cause: You're using the wrong webhook secret or passing parsed JSON instead of the raw request body to the verification function.Fix: Copy the exact endpoint secret from Stripe Dashboard → Developers → Webhooks, and ensure your framework passes the raw body (not JSON-parsed) to stripe.Webhook.constructEvent().
---
Real Console Errors
Here are the exact errors you'll see at 2am:
``` Error: No signatures found matching the expected signature for payload. ```
``` StripeSignatureVerificationError: No signatures found matching the expected signature for payload. Webhook secret (***_test_abc123) may be invalid. ```
``` Unhandled rejection StripeSignatureVerificationError: Timestamp outside the tolerance window ```
``` Error: Unable to extract timestamp and signatures from header ```
``` 401 Unauthorized - Webhook endpoint signature verification failed ```
---
The Problem: Side-by-Side Code Comparison
❌ BROKEN CODE
```javascript
// Express.js - WRONG
app.post('/webhook', express.json(), (req, res) => {
const sig = req.headers['stripe-signature'];
const secret = process.env.STRIPE_WEBHOOK_SECRET;
try {
// BUG: req.body is already parsed JSON, not raw bytes
const event = stripe.webhooks.constructEvent(
req.body, // ❌ WRONG - this is a JavaScript object
sig,
secret
);
res.json({received: true});
} catch (err) {
res.status(400).send(Webhook Error: ${err.message});
}
});
```
✅ FIXED CODE
```javascript
// Express.js - CORRECT
app.post(
'/webhook',
express.raw({type: 'application/json'}), // ✅ Raw body middleware
(req, res) => {
const sig = req.headers['stripe-signature'];
const secret = process.env.STRIPE_WEBHOOK_SECRET;
try {
// ✅ CORRECT - req.body is Buffer with raw bytes
const event = stripe.webhooks.constructEvent(
req.body, // Now this is the raw request body
sig,
secret
);
res.json({received: true});
} catch (err) {
res.status(400).send(Webhook Error: ${err.message});
}
}
);
```
Additional Framework Examples
Next.js API Route - BROKEN: ```javascript export default async (req, res) => { const event = stripe.webhooks.constructEvent( JSON.stringify(req.body), // ❌ Double-stringifying causes signature mismatch req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET ); }; ```
Next.js API Route - FIXED: ```javascript export const config = { api: { bodyParser: false } // ✅ Disable auto-parsing };
export default async (req, res) => { const buf = await getRawBody(req); // Use raw-body library const event = stripe.webhooks.constructEvent( buf, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET ); }; ```
---
Root Causes Checklist
1. Wrong Webhook Secret: You copied the signing secret instead of the endpoint secret. In Stripe Dashboard, go Developers → Webhooks → click your endpoint → copy "Signing secret" (starts with whsec_)
2. Middleware Parsing Body: Express's express.json() or similar middleware automatically parses the raw body into a JavaScript object. Stripe's constructEvent() requires the original raw bytes to verify the signature. Use express.raw() instead.
3. Environment Variable Typo: Verify your .env file has STRIPE_WEBHOOK_SECRET=whsec_xxxxx (test mode secret should include _test_)
4. Using Live Secret in Test Environment: If you're testing with test webhooks but using your live signing secret (or vice versa), verification fails. Ensure environment matches.
5. Timestamp Tolerance: Stripe rejects webhooks older than 5 minutes. If your server clock is severely out of sync, synchronize with NTP.
---
Still broken? Check these too
process.env.STRIPE_WEBHOOK_SECRET might be undefined; add explicit logging before constructEvent()---
Debug Steps
```javascript // Add temporary logging (remove after debugging) console.log('Secret:', process.env.STRIPE_WEBHOOK_SECRET?.slice(-8)); console.log('Signature header:', req.headers['stripe-signature']?.slice(0, 20)); console.log('Body type:', typeof req.body, 'Is Buffer:', Buffer.isBuffer(req.body)); ```
If Body type: object appears, you're parsing too early. Remove the JSON parser before the webhook route.
---
Official Resources
---
Found a different variation? Drop it in the comments.