Stripe: webhook signature verification failing [2026 fix]
Webhook signature mismatch from endpoint secret misconfiguration or timestamp validation. Use correct signing secret and verify within 5-minute window.
Stripe: Webhook Signature Verification Failing [2026 Fix]
TL;DR
Cause: Your endpoint is using the wrong signing secret (often the API key instead of the webhook endpoint secret) or the request timestamp is outside the 5-minute verification window.Fix: Replace your secret with the correct whsec_* or we_* endpoint secret from Stripe Dashboard → Developers → Webhooks, and ensure server time is synchronized.
---
Exact Error Messages You'll See
These are real console outputs from webhook verification failures:
``` Error: No signatures found matching the expected signature for payload. ```
``` Stripe.error.SignatureVerificationError: Timestamp outside the tolerance window ```
``` Webhook signature verification failed: Invalid signature ```
``` Error: Unable to extract timestamp and signatures from header 'stripe-signature' ```
``` Fatal error in Stripe webhook handler: Signature verification failed for event evt_* ```
---
The Problem: Broken vs. Fixed Code
❌ BROKEN CODE (Common Mistakes)
```javascript // Mistake 1: Using API key instead of webhook secret const stripe = require('stripe')(process.env.STRIPE_API_KEY); const secret = process.env.STRIPE_API_KEY; // WRONG!
app.post('/webhook', (req, res) => {
try {
const event = stripe.webhooks.constructEvent(
req.rawBody,
req.headers['stripe-signature'],
secret // This will ALWAYS fail
);
} catch (err) {
return res.status(400).send(Webhook Error: ${err.message});
}
});
```
```javascript // Mistake 2: Not using raw body buffer app.use(express.json()); // Parsed JSON breaks signature
app.post('/webhook', (req, res) => { const event = stripe.webhooks.constructEvent( req.body, // Already parsed - signature won't match! req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET ); }); ```
✅ FIXED CODE
```javascript // Correct implementation const stripe = require('stripe')(process.env.STRIPE_API_KEY); const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; // whsec_live_* or whsec_test_*
// CRITICAL: Handle webhook endpoint BEFORE express.json()
app.post(
'/webhook',
express.raw({type: 'application/json'}), // Raw buffer, not parsed
(req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body, // Raw buffer from express.raw()
sig,
webhookSecret // MUST be whsec_* endpoint secret
);
} catch (err) {
console.error(Webhook signature verification failed: ${err.message});
return res.status(400).send(Webhook Error: ${err.message});
}
// Handle event
switch (event.type) {
case 'payment_intent.succeeded':
// Process payment
break;
case 'charge.failed':
// Handle failure
break;
}
res.json({received: true});
}
);
// Other routes CAN use express.json() app.use(express.json()); app.post('/api/other', (req, res) => { // This is fine }); ```
Key differences:
1. express.raw({type: 'application/json'}) middleware preserves the raw request body
2. webhookSecret starts with whsec_ (not your API key)
3. Webhook endpoint is defined BEFORE express.json()
---
Verification Checklist
1. Get the right secret:
- Go to [Stripe Dashboard](https://dashboard.stripe.com/webhooks) → Developers → Webhooks
- Find your endpoint URL
- Click "Reveal" next to "Signing secret"
- Copy the full whsec_* value
2. Verify environment variable: ```bash echo $STRIPE_WEBHOOK_SECRET # Should output: whsec_live_ABC123... or whsec_test_ABC123... # NOT your API key (sk_live_* or sk_test_*) ```
3. Check server time synchronization: ```javascript // Stripe rejects signatures older than 5 minutes console.log(new Date().toISOString()); // Should be within ±5 minutes of actual time ```
4. Test with Stripe CLI locally: ```bash stripe listen --forward-to localhost:3000/webhook stripe trigger payment_intent.succeeded ```
---
Still broken? Check these too
1. Multiple webhooks registered: You might be testing against the wrong endpoint. Verify in Dashboard that the URL exactly matches your live server (no staging/dev URLs mixed in).
2. Raw body middleware conflict: If using third-party middleware that modifies the request body before your webhook handler, it will break the signature. Load webhook route before any body-parsing middleware [related](/?guide=express-middleware-order).
3. Stripe library version: We're certain about behavior in stripe@14.0.0+ (2024+). If you're on stripe@8.x or earlier, timestamp tolerance may differ—check official docs for your version [related](/?guide=stripe-version-compatibility).
---
References
Found a different variation? Drop it in the comments—we update this guide based on real production issues.