Stripe: webhook signature verification failing [2026 fix]
Webhook signature mismatch: timestamp too old or signing secret wrong. Verify endpoint secret matches live/test mode and clock skew < 5min.
Stripe: Webhook Signature Verification Failing [2am Emergency Fix]
TL;DR
Cause: Your webhook endpoint is using the wrong signing secret or your server's clock is skewed >5 minutes from Stripe's servers. Fix: Verify you're using the correctwhsec_ or wh_secret_ for your environment (test vs. live) and sync your server time with NTP.---
Exact Error Messages from Console
``` Error: No signatures found matching the expected signature for payload. ```
``` StripeSignatureVerificationError: Timestamp outside the tolerance window at verifySignatureHeader (/node_modules/stripe/lib/utils.js:156:12) ```
``` Error: Invalid signature: expected sig_xxxx but received sig_yyyy ```
``` Webhook endpoint failed: 403 Forbidden - signature verification failed for [evt_123abc] ```
``` stripe.error.SignatureVerificationError: Unexpected signed header format ```
---
Broken Code vs. Exact Fix
❌ BROKEN (Most Common)
```javascript // webhooks/stripe.js const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
// Using wrong secret or no secret at all
const sig = req.headers['stripe-signature'];
try {
// WRONG: Using API key instead of webhook secret
const event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_SECRET_KEY // ← THIS IS THE BUG
);
res.json({received: true});
} catch (err) {
return res.status(400).send(Webhook Error: ${err.message});
}
});
```
✅ FIXED
```javascript // webhooks/stripe.js const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; // whsec_test_xxx or whsec_live_xxx
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
try {
// CORRECT: Using webhook endpoint secret
const event = stripe.webhooks.constructEvent(
req.body,
sig,
endpointSecret // ← WEBHOOK SECRET, NOT API KEY
);
// Process event
switch (event.type) {
case 'payment_intent.succeeded':
handlePaymentSuccess(event.data.object);
break;
case 'charge.failed':
handleChargeFailed(event.data.object);
break;
}
res.json({received: true});
} catch (err) {
console.error(Webhook signature verification failed: ${err.message});
return res.status(400).send(Webhook Error: ${err.message});
}
});
```
Critical Differences
| Aspect | Broken | Fixed |
|--------|--------|-------|
| Secret Source | STRIPE_SECRET_KEY (API key) | STRIPE_WEBHOOK_SECRET (webhook secret) |
| Secret Format | sk_test_xxx or sk_live_xxx | whsec_test_xxx or whsec_live_xxx |
| Where to Get | Developers → API Keys | Webhooks → Signing Secret |
| Environment | Must match (test/live) | MUST match (test/live) |
---
Version-Specific Notes
STRIPE_WEBHOOK_SECRET format starts with whsec_---
Additional Diagnostics
```bash
1. Verify your endpoint secret is loaded
echo $STRIPE_WEBHOOK_SECRETOutput should be: whsec_test_xxx or whsec_live_xxx
2. Check server clock skew (signature tolerance is ±5 minutes)
dateShould match: https://worldtimeapi.org/api/timezone/Etc/UTC
3. Confirm webhook is hitting correct environment
Test webhook: use whsec_test_xxx
Live webhook: use whsec_live_xxx
```---
Still Broken? Check These Too
1. [Raw Body Parsing Issue](/?guide=express-raw-body) — Stripe requires unparsed Buffer body. If you're using express.json() middleware before webhook route, signature will fail. Move webhook route *before* global middleware or use express.raw({type: 'application/json'}).
2. Test vs. Live Mode Mismatch — Dashboards often switch modes. Confirm your .env has whsec_test_xxx for development. Dashboard: Settings → Webhooks → click endpoint → copy signing secret.
3. [Webhook Endpoint URL Wrong](/?guide=stripe-webhook-url) — Signature is tied to endpoint URL. If you changed from example.com/webhook to example.com/stripe-webhook, webhook was never re-sent. Re-trigger in Stripe Dashboard or wait for next event.
---
Official Resources
[Stripe Webhook Signatures Documentation](https://stripe.com/docs/webhooks/signatures)
---
Found a different variation? Drop it in the comments below—every 2am horror story helps the next person.