Stripe: webhook signature verification failing [2026 fix]
Webhook signature verification fails when endpoint secret is missing, misconfigured, or request body is altered. Use raw request body and correct endpoint secret from Dashboard.
Stripe: Webhook Signature Verification Failing [2am Emergency Fix]
TL;DR
Cause: Your code is using the wrong endpoint secret, or the request body is being parsed/modified before signature verification. Fix: Verify you're using the raw unparsed request body and the correct endpoint secret from Stripe Dashboard → Webhooks → Signing secret.---
Exact Console Error Messages
Error 1: Missing Endpoint Secret
``` Error: Missing endpoint secret at Function.constructEvent (/node_modules/stripe/lib/webhooks.js:112:45) at POST /api/webhooks/stripe (webhook.js:8:12) ```Error 2: Signature Mismatch
``` Error: No signatures found matching the expected signature for payload. Please check that the endpoint's signing secret is correct. at Function.verifySignature (/node_modules/stripe/lib/webhooks.js:156:89) ```Error 3: Invalid Timestamp
``` Error: Timestamp outside the tolerance zone at Function.constructEvent (/node_modules/stripe/lib/webhooks.js:189:34) ```Error 4: Malformed Webhook Header
``` StripeSignatureVerificationError: Unable to extract timestamp and signatures from header at verifySignature (/node_modules/stripe/lib/webhooks.js:78:11) ```Error 5: Request Body Already Parsed
``` Error: No signatures found matching the expected signature for payload. Webhook payload must be passed as raw request body string, not parsed JSON ```---
Broken Code → Fixed Code
❌ BROKEN: Using Parsed JSON Body
```javascript
// Express.js - WRONG
app.post('/api/webhooks/stripe', express.json(), (req, res) => {
const sig = req.headers['stripe-signature'];
const endpointSecret = 'whsec_test_secret';
try {
// ❌ PROBLEM: req.body is already parsed JSON
// Stripe signature is calculated on RAW bytes
const event = stripe.webhooks.constructEvent(
req.body, // This is an object, not a string!
sig,
endpointSecret
);
} catch (err) {
console.error('Webhook Error:', err.message);
res.status(400).send(Webhook Error: ${err.message});
}
});
```
✅ FIXED: Using Raw Body Middleware
```javascript
// Express.js - CORRECT
app.post('/api/webhooks/stripe',
// Use raw body BEFORE json() parser
express.raw({type: 'application/json'}),
(req, res) => {
const sig = req.headers['stripe-signature'];
// Fetch from environment variable, never hardcode
const endpointSecret = process.env.STRIPE_ENDPOINT_SECRET;
try {
// ✅ CORRECT: req.body is raw Buffer/string
const event = stripe.webhooks.constructEvent(
req.body, // Raw bytes maintained
sig,
endpointSecret
);
// Handle specific events
switch (event.type) {
case 'payment_intent.succeeded':
// Process payment
break;
case 'charge.refunded':
// Process refund
break;
}
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 // Using publishable key or API key instead of signing secret const endpointSecret = process.env.STRIPE_API_KEY; // ❌ WRONG ```
✅ FIXED: Correct Secret Source
```javascript // Stripe Dashboard → Developers → Webhooks → (click endpoint) → Signing secret const endpointSecret = process.env.STRIPE_ENDPOINT_SECRET; // ✅ CORRECT // Example format: whsec_live_51234567890abcdef or whsec_test_... ```
❌ BROKEN: Middleware Order Issue
```javascript app.use(express.json()); // Parses ALL incoming JSON
app.post('/api/webhooks/stripe', (req, res) => { // ❌ req.body is already parsed, can't verify signature const event = stripe.webhooks.constructEvent( req.body, sig, secret ); }); ```
✅ FIXED: Route-Specific Raw Body
```javascript // Webhook endpoint gets raw body app.post('/api/webhooks/stripe', express.raw({type: 'application/json'}), webhookHandler );
// Other endpoints get parsed JSON app.use(express.json()); app.post('/api/other', otherHandler); ```
---
Still Broken? Check These Too
1. Timestamp Tolerance: Stripe rejects webhooks older than 5 minutes. If your server clock is off by >5 min, verification fails. Run date and sync system time.
2. Multiple Endpoint Secrets: You might have multiple webhook endpoints in Stripe Dashboard. Verify you're using the secret for the endpoint that's actually receiving the request. Each endpoint has a unique secret.
3. Environment Variable Not Loading: Confirm process.env.STRIPE_ENDPOINT_SECRET is actually set. Add console.log('Secret:', process.env.STRIPE_ENDPOINT_SECRET?.substring(0, 10)) to verify it's loading (never log the full secret).
4. Reverse Proxy/Load Balancer Modifying Body: If behind nginx/AWS ALB, ensure raw body is passed through unchanged. Some proxies may gzip or re-encode, breaking the signature.
5. Testing with Wrong Mode Secret: Ensure your test webhook uses whsec_test_* secret, production webhooks use whsec_live_*. Using test secret to verify live webhooks will fail.
---
Version Note
This guide covers stripe npm package v13+. [Check your version](/?guide=stripe-version-check). Stripe.js webhook handling is consistent across versions, but middleware setup differs for Express v4 vs v5—explicitly test your Express major version if unsure.
---
Related Guides
---
Official Reference
📖 [Stripe Webhook Signature Verification](https://stripe.com/docs/webhooks/signatures)
---
Found a different variation? Drop it in the comments.