Stripe: webhook signature verification failing [2026 fix]
Webhook signature mismatch caused by endpoint secret misconfiguration or timestamp drift—verify secret matches dashboard and check server time sync.
Stripe: Webhook Signature Verification Failing [2026 Fix]
TL;DR
Cause: Your endpoint secret doesn't match Stripe's dashboard, or your server's system clock is >5 minutes out of sync. Fix: Copy the *exact* signing secret from Stripe Dashboard → Webhooks → [Your Endpoint] → Reveal, paste into your.env, restart your app, and verify ntpdate -u pool.ntp.org on your server.---
Real Console Error Messages
``` Error: No signatures found matching the expected signature for payload. Received signature(s): t=1704067234,v1=abcd1234... ```
``` Stripe.error.SignatureVerificationError: No signatures found matching the expected signature for payload at VerifySignatureHeader (/node_modules/@stripe/stripe-js/lib/webhook.js:142:15) ```
``` wt_signature=t=1704067234,v1=xyz789; expected=t=1704067234,v1=abc123 [MISMATCH] ```
``` Error: Timestamp outside the tolerance window. received: 1704067234, now: 1704067534, tolerance: 300 ```
``` SignatureVerificationError: Unable to extract timestamp and signatures from header 'stripe-signature' ```
---
Broken vs. Fixed Code
❌ BROKEN: Missing or Wrong Secret
```javascript const stripe = require('stripe')('sk_live_...');
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
// Using hardcoded/wrong secret or undefined env var
const endpointSecret = 'whsec_1234567890abcdef'; // ← WRONG! Copy-pasted once, now stale
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
req.headers['stripe-signature'],
endpointSecret // ← Stripe rejects because this doesn't match their records
);
} catch (err) {
return res.status(400).send(Webhook Error: ${err.message});
}
});
```
✅ FIXED: Correct Secret + Time Sync
```javascript const stripe = require('stripe')('sk_live_...');
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
// Pull EXACT secret from Stripe Dashboard → Webhooks → [Endpoint Name] → Reveal
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
if (!endpointSecret) {
console.error('STRIPE_WEBHOOK_SECRET not set in environment');
return res.status(500).send('Webhook handler misconfigured');
}
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
req.headers['stripe-signature'],
endpointSecret // ← Matches Stripe's records exactly
);
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).send(Webhook Error: ${err.message});
}
// Process event safely
res.json({received: true});
});
```
What changed: 1. Load secret from environment variable (not hardcoded) 2. Validate the secret exists before using it 3. Copy secret *directly* from Stripe Dashboard → don't manually type it 4. Ensure your server's clock is synced (see "Still broken?" below)
---
Step-by-Step Verification
1. Verify Your Endpoint Secret (Critical)
whsec_.env file: STRIPE_WEBHOOK_SECRET=whsec_...2. Confirm Environment Variable Loading
```javascript console.log('Secret length:', process.env.STRIPE_WEBHOOK_SECRET?.length); console.log('First 10 chars:', process.env.STRIPE_WEBHOOK_SECRET?.substring(0, 10)); // Should show: whsec_ ```
3. Check Server Time Sync
```bash
On Linux/macOS
ntpdate -u pool.ntp.orgCheck current time
dateOn Docker/containers, restart to inherit host time
docker-compose down && docker-compose up -d ```Stripe rejects signatures older than 5 minutes. If your server is >5 min behind, all webhooks fail.
---
Still broken? Check these too
1. Webhook Endpoint URL Mismatch
- Stripe sends to the exact URL registered in Dashboard - If you changed your domain/port, update the endpoint URL in Dashboard → Webhooks - Test delivery shows400 Webhook handler misconfigured → check URL is reachable2. Body Parsing Middleware Conflict
- If you useexpress.json() *before* the webhook route, Stripe's raw body gets consumed
- Solution: Register webhook route *before* global JSON middleware, or move to separate router
- See [related webhook routing guide](/?guide=express-middleware-order)3. Multiple Endpoint Secrets (Development vs. Production)
- Stripe generates differentwhsec_ for test and live modes
- Confirm you're using the secret from the *correct* mode (test/live toggle in Dashboard)
- Mismatched mode = signature always fails
- See [related test/live mode guide](/?guide=stripe-test-live-setup)---
Uncertainty Callout
⚠️ Version Note: This guide covers @stripe/stripe-js v2.x and stripe Node package v10+. If you're on v8.x or earlier, the webhook signature format differs slightly—[check official docs](https://stripe.com/docs/webhooks/signatures) for your version's exact implementation.
---
Official Resources
---
Found a different variation? Drop it in the comments
Escalating to production at 2am? Missing edge case? Share your exact error message and environment setup below—we'll add it to this guide.