Stripe: webhook signature verification failing [2026 fix]
Webhook signature mismatch from timestamp drift or missing secret—regenerate endpoint key and verify clock sync.
Stripe: Webhook Signature Verification Failing [2am Emergency Fix]
TL;DR
Cause: Your webhook endpoint secret is stale, your server clock is drifting >5 minutes, or you're reading the raw body after Express already parsed it. Fix: Regenerate the endpoint signing secret in Stripe Dashboard, sync your system time withntpdate, and ensure you're using the raw request body before any JSON parsing middleware.---
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 't=..., v1=...' ```
``` Webhook signature verification failed: Timestamp outside the tolerance window ```
``` Error: Invalid signature: signature version v1 not found in header ```
``` ReferenceError: Cannot read property 'raw' of undefined at verifySignature ```
---
The Problem: Broken vs Fixed Code
❌ BROKEN CODE (Common Mistake #1: Parsed Body)
```javascript // Express app.js - WRONG app.use(express.json()); // This parses the body!
app.post('/webhook', (req, res) => {
const sig = req.headers['stripe-signature'];
const secret = process.env.STRIPE_WEBHOOK_SECRET;
try {
// req.body is now a parsed object, NOT raw bytes
const event = stripe.webhooks.constructEvent(
req.body, // ❌ WRONG: Already stringified/parsed
sig,
secret
);
} catch (err) {
res.status(400).send(Webhook Error: ${err.message});
}
});
```
✅ FIXED CODE #1: Raw Body Before Parsing
```javascript // Express app.js - CORRECT app.use(express.json()); // Normal middleware
// THIS MUST COME BEFORE express.json()
app.post('/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.headers['stripe-signature'];
const secret = process.env.STRIPE_WEBHOOK_SECRET;
try {
// req.body is raw Buffer here
const event = stripe.webhooks.constructEvent(
req.body, // ✅ CORRECT: Raw bytes
sig,
secret
);
// Process event...
res.json({ received: true });
} catch (err) {
res.status(400).send(Webhook Error: ${err.message});
}
}
);
```
❌ BROKEN CODE (Common Mistake #2: Stale Secret)
```javascript const stripe = require('stripe')(process.env.STRIPE_KEY);
// Secret loaded once at startup, never rotated const WEBHOOK_SECRET = 'whsec_live_abc123...'; // Hardcoded or from .env loaded 30 days ago ```
✅ FIXED CODE #2: Refresh & Verify Secret
```javascript const stripe = require('stripe')(process.env.STRIPE_KEY);
// 1. Go to Stripe Dashboard → Developers → Webhooks → Click endpoint // 2. Scroll to "Signing secret" → Click "Reveal" → Copy fresh value const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET; // Updated today
// Add this to your startup logs console.log('Webhook secret loaded (first 8 chars): whsec_' + WEBHOOK_SECRET.substring(7, 15) + '...');
// Verify it matches what Stripe has if (!WEBHOOK_SECRET || !WEBHOOK_SECRET.startsWith('whsec_')) { throw new Error('STRIPE_WEBHOOK_SECRET not set or invalid format'); } ```
❌ BROKEN CODE (Common Mistake #3: Clock Drift)
```javascript // No timestamp validation—server clock is 10 minutes behind const event = stripe.webhooks.constructEvent( req.body, sig, secret // Default tolerance: 5 minutes. If server clock drifts >5min, fails. ); ```
✅ FIXED CODE #3: Sync Time & Add Tolerance (Last Resort)
```bash
On your server - fix the root cause
sudo ntpdate -s time.nist.gov # Linux/Macor
sudo timedatectl set-ntp true # Systemd systems ``````javascript // Only as temporary workaround while fixing clock const event = stripe.webhooks.constructEvent( req.body, sig, secret, 600 // 10-minute tolerance (default is 300 = 5 min) // ⚠️ This is a bandaid—fix your system clock! ); ```
---
Immediate Checklist (Do These NOW)
1. Regenerate secret: Dashboard → Developers → Webhooks → [your endpoint] → "Signing secret" → Reveal → Copy
2. Update .env: Paste fresh secret, restart app
3. Check server time: Run date command—should match date -u (within seconds)
4. Verify middleware order: Ensure express.raw() middleware loads BEFORE express.json() for webhook route
5. Check logs: Look for timestamp mismatch errors: Timestamp outside the tolerance window
---
Still Broken? Check These Too
1. [Webhook endpoint not receiving requests at all](/?guide=stripe-webhook-not-firing) — Different root cause, but overlapping troubleshooting
2. [Stripe API version mismatch in your webhook](/?guide=stripe-api-version-errors) — Can cause signature issues on version updates
3. Database/environment issues preventing secret load — Verify console.log(process.env.STRIPE_WEBHOOK_SECRET) outputs correct value
---
Version Notes
Stripe SDK: As of v15+ (2024+), behavior is consistent. If using v12 or earlier, I'm uncertain about edge cases with Buffer handling—update to current version if possible.
Node.js: Express middleware order matters on all versions. This guide applies to Express 4.x+.
---
Official Resources
[Stripe Webhooks Security Documentation](https://stripe.com/docs/webhooks/signatures)
[Express Raw Body Middleware](https://expressjs.com/en/api/express.html#express.raw)
---
Found a different variation? Drop it in the comments.