Stripe: webhook signature verification failing [2026 fix]
Webhook signature mismatch: endpoint secret mismatch or missing raw body. Verify STRIPE_WEBHOOK_SECRET env var and pass raw request body to verifyHeader().
Stripe: Webhook Signature Verification Failing [2am Emergency Fix]
TL;DR
Cause: Your endpoint secret doesn't match what Stripe is using, or you're passing the parsed JSON body instead of the raw request body to signature verification. Fix: VerifySTRIPE_WEBHOOK_SECRET matches your dashboard endpoint, and always pass req.rawBody (not req.body) to stripe.webhooks.constructEvent().---
Exact Error Messages You'll See
``` Error: No signatures found matching the expected signature for payload. ```
``` StripeSignatureVerificationError: Unable to extract timestamp and signatures from header ```
``` Error: Webhook signature verification failed. Webhook signing secret does not match. ```
``` Signature verification failed. Invalid signature for request payload. ```
``` StripeSignatureVerificationError: No match found for signature ```
---
Broken Code → Fixed Code
Problem #1: Using Parsed Body Instead of Raw Body
BROKEN: ```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: req.body is already parsed JSON
const event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
// ...
} catch (err) {
res.status(400).send(Webhook Error: ${err.message});
}
});
```
FIXED: ```javascript const express = require('express'); 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'];
try {
// ✅ CORRECT: req.body is Buffer/raw bytes
const event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
// ...
} catch (err) {
res.status(400).send(Webhook Error: ${err.message});
}
}
);
```
Key difference: Use express.raw({ type: 'application/json' }) NOT express.json().
Problem #2: Environment Variable Mismatch
BROKEN: ```javascript const endpointSecret = 'whsec_test123'; // ❌ Hardcoded
const event = stripe.webhooks.constructEvent( req.body, sig, endpointSecret ); ```
FIXED: ```javascript const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
if (!endpointSecret) { throw new Error('STRIPE_WEBHOOK_SECRET environment variable missing'); }
const event = stripe.webhooks.constructEvent( req.body, sig, endpointSecret ); ```
Verification steps:
1. Go to Stripe Dashboard → Developers → Webhooks
2. Find your endpoint, click it
3. Click "Reveal" next to Signing secret
4. Copy the whsec_... value
5. Set it as STRIPE_WEBHOOK_SECRET in your .env file
6. Restart your server
7. Verify with echo $STRIPE_WEBHOOK_SECRET in production
Problem #3: Middleware Order (Express)
BROKEN: ```javascript // ❌ JSON parsing happens first, consuming the raw body app.use(express.json());
app.post('/webhook', (req, res) => { const event = stripe.webhooks.constructEvent(req.body, sig, secret); }); ```
FIXED: ```javascript // ✅ Raw parsing for webhook BEFORE global JSON middleware app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const event = stripe.webhooks.constructEvent(req.body, sig, secret); } );
// Other routes get normal JSON parsing app.use(express.json());
app.post('/api/other', (req, res) => { // req.body is parsed here }); ```
---
Still Broken? Check These Too
1. Trailing/Leading Whitespace in Secret: Copy your webhook secret directly from the Dashboard. Paste into .env without quotes: STRIPE_WEBHOOK_SECRET=whsec_abc123... (not "whsec_abc123..."). Verify no whitespace with echo "${STRIPE_WEBHOOK_SECRET}" | od -c.
2. Timestamp Too Old: Stripe rejects signatures older than 5 minutes by default. If your server time is drifting, run ntpdate -s time.nist.gov or check system clock: date. [Related webhook testing guide](/?guide=stripe-webhook-testing).
3. Multiple Endpoints Configured: You may have configured this webhook in both test and live mode with different secrets. Verify you're using the correct secret for the mode (test vs. live). Dashboard shows both—make sure env vars match the mode you're testing.
4. Reverse Proxy Stripping Headers: If behind nginx/CloudFlare, ensure stripe-signature header isn't being stripped. Check with console.log(req.headers) in your handler.
5. Stripe SDK Version Mismatch: [Verify your stripe package version](/?guide=stripe-package-versions) matches your code. As of 2026, we're on stripe@17+, but older code might expect stripe@11 APIs.
---
Verification Checklist
express.raw() not express.json() for webhook routeSTRIPE_WEBHOOK_SECRET copied exactly from Dashboard (test or live)constructEvent(), not parsed JSON---
Official Resources
[Stripe Webhook Signature Verification Docs](https://stripe.com/docs/webhooks/signatures)
---
Found a different variation? Drop it in the comments.