Stripe: webhook signature verification failing [2026 fix]
Webhook signature mismatch: verify your endpoint secret matches Stripe dashboard and request body isn't modified before verification.
Stripe: Webhook Signature Verification Failing [2026 Fix]
TL;DR
Cause: Your endpoint secret doesn't match Stripe's dashboard value, or the raw request body was parsed/modified before signature verification. Fix: Use the exact raw body bytes for verification and confirm yourwhSecret matches the Stripe dashboard's "Signing secret" for that endpoint.---
Exact Error Messages
Here are the real console outputs you'll see:
``` Error: No signatures found matching the expected signature for payload. EventId: evt_1234abcd. Details: (end) ```
``` StripeSignatureVerificationError: No matching signatures found at WebhookEndpoint.constructEvent (webhook.js:156:23) at verifyWebhook (server.js:42:8) ```
``` Warning: Webhook signature verification failed. Received sig: t=1704067200,v1=abc123... Expected: t=1704067200,v1=xyz789... ```
``` Error: Invalid signature for event evt_evt_12345abcDefgh. Make sure your signing secret matches the endpoint's Signing secret in the Dashboard. ```
```
{"error": {"message": "Invalid sig_secret or sig_header"}}
```
---
Broken Code vs. Fix
Problem 1: Parsed Body Before Verification
BROKEN:
```javascript
app.post('/webhook', (req, res) => {
const body = req.body; // ❌ Already parsed JSON
const sig = req.headers['stripe-signature'];
try {
const event = stripe.webhooks.constructEvent(
body, // WRONG: needs raw bytes
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
res.json({ received: true });
} catch (err) {
res.status(400).send(Error: ${err.message});
}
});
```
FIXED:
```javascript
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const body = req.body; // ✅ Raw buffer from express.raw()
const sig = req.headers['stripe-signature'];
try {
const event = stripe.webhooks.constructEvent(
body, // Correct: raw bytes
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
res.json({ received: true });
} catch (err) {
res.status(400).send(Error: ${err.message});
}
});
```
Problem 2: Wrong Endpoint Secret
BROKEN: ```javascript const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// Using global secret instead of endpoint-specific secret const whSecret = process.env.STRIPE_SECRET_KEY; // ❌ WRONG
const event = stripe.webhooks.constructEvent(body, sig, whSecret); ```
FIXED: ```javascript const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// Use the signing secret from Dashboard > Webhooks > [Your Endpoint] > Signing secret const whSecret = process.env.STRIPE_WEBHOOK_SECRET; // ✅ Correct
const event = stripe.webhooks.constructEvent(body, sig, whSecret); ```
Problem 3: Framework Middleware Parsing
BROKEN (with bodyParser): ```javascript const bodyParser = require('body-parser');
app.use(bodyParser.json()); // ❌ Parses ALL routes before webhook app.post('/webhook', (req, res) => { const event = stripe.webhooks.constructEvent( req.body, // Already parsed, signature fails req.headers['stripe-signature'], whSecret ); }); ```
FIXED: ```javascript const bodyParser = require('body-parser'); const express = require('express');
// Webhook MUST be before bodyParser app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { const event = stripe.webhooks.constructEvent( req.body, // ✅ Raw bytes req.headers['stripe-signature'], whSecret ); });
// Then apply bodyParser for other routes app.use(bodyParser.json()); app.post('/api/other', (req, res) => { /* ... */ }); ```
---
Still Broken? Check These Too
1. Webhook secret copied wrong: Open Stripe Dashboard → Developers → Webhooks → Your endpoint → "Signing secret". Copy the full string (starts with whsec_). Don't confuse it with API key.
2. Multiple webhook endpoints: You're listening on /webhook but Stripe is configured for /webhooks (trailing 's'). Check Dashboard → endpoint URL exactly matches your server URL.
3. Request body encoding mismatch: Stripe sends UTF-8. If you're on Node.js with a proxy/load balancer, verify it's not converting encoding. Test with: console.log(typeof req.body); // should be 'object' for express.raw()
---
Version-Specific Notes
Express 4.16.0+: express.raw() is built-in (no separate middleware needed).
Stripe SDK < v8.x: constructEvent() may not exist; use webhooks.constructEvent() instead. Exact behavior differs—check your package.json.
Node.js < 14: Explicit Buffer handling may differ. Uncertain if older versions have issues here—test in staging.
---
Related Reading
Official Resources
[Stripe Webhook Signature Verification Documentation](https://stripe.com/docs/webhooks/signatures)
---
Found a different variation? Drop it in the comments.