Stripe: webhook signature verification failing [2026 fix]
Webhook signature mismatch caused by timestamp validation or missing/incorrect endpoint secret—verify your signing secret matches Stripe dashboard and check request timestamp tolerance.
Stripe: Webhook Signature Verification Failing [2am Emergency Fix]
TL;DR
Cause: Your endpoint secret is wrong, outdated, or your server's clock is >5 minutes skewed from Stripe's servers. Fix: Regenerate the endpoint secret in Stripe Dashboard and verify your server time is synchronized.---
Console Error Messages (Real Examples)
``` Error: No signatures found matching the expected signature for payload. ```
``` Error: Webhook Error: Timestamp outside the tolerance zone ```
``` Signature verification failed. Webhook signature does not match ```
``` Error: Invalid signature. Webhook secret mismatch detected ```
``` 401 Unauthorized: webhook_secret does not match endpoint configuration ```
---
The Problem: Side-by-Side Code Comparison
❌ BROKEN CODE
```javascript const stripe = require('stripe')('sk_test_...'); const endpointSecret = 'we_test_123abc'; // WRONG: old or incorrect secret
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
try {
const event = stripe.webhooks.constructEvent(
req.body,
sig,
endpointSecret
);
console.log('Webhook received:', event.type);
res.json({received: true});
} catch (err) {
console.log('Webhook error:', err.message); // Logs but doesn't indicate root cause
res.status(400).send(Webhook Error: ${err.message});
}
});
```
Problems:
1. endpointSecret hardcoded and potentially stale
2. No validation of server time synchronization
3. No logging of timestamp differences
4. Signature construction doesn't account for clock skew tolerance
✅ FIXED CODE
```javascript const stripe = require('stripe')('sk_test_...'); // Load from environment variable, regenerated from Dashboard const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
if (!endpointSecret) { throw new Error('STRIPE_WEBHOOK_SECRET not set in environment'); }
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
try {
// Stripe tolerance is 5 minutes (300 seconds) by default
const event = stripe.webhooks.constructEvent(
req.body,
sig,
endpointSecret
);
console.log(✓ Webhook verified: ${event.type} at ${new Date().toISOString()});
res.json({received: true});
} catch (err) {
if (err.message.includes('Timestamp outside the tolerance')) {
console.error(⚠ Clock skew detected. Server time: ${new Date().toISOString()});
console.error('Action: Sync server time with NTP pool or AWS Systems Manager');
}
console.error(Webhook signature error: ${err.message});
res.status(400).send(Webhook Error: ${err.message});
}
});
```
Key Fixes:
1. Secret loaded from process.env (never hardcoded)
2. Explicit validation that secret exists
3. Clear logging with timestamps
4. Specific error handling for clock skew
---
Step-by-Step Fix (Right Now)
1. Open [Stripe Dashboard](https://dashboard.stripe.com) → Developers → Webhooks
2. Find your endpoint (e.g., https://yourapp.com/webhook)
3. Click "Reveal" next to "Signing secret" — copy the full whsec_test_... string
4. Update your .env file:
```
STRIPE_WEBHOOK_SECRET=whsec_test_YOUR_ACTUAL_SECRET_HERE
```
5. Restart your server (npm start or node app.js)
6. Verify server time:
```bash
date
ntpdate -q pool.ntp.org # Check NTP sync
```
7. Trigger a test webhook from Stripe Dashboard → Send test event
If still failing → check "Still broken?" section below.
---
Still Broken? Check These Too
1. Multiple Endpoints with Different Secrets
Issue: You have multiple webhook endpoints in Stripe (dev, staging, prod) but deploying to the wrong one. Fix: Verify the endpoint URL matches your deployment. Useconsole.log(req.originalUrl) to confirm which endpoint received the request.2. Middleware Order Problem
Issue: If you're usingexpress.json() before express.raw(), the body gets parsed and signature breaks.
Fix: Ensure webhook route uses express.raw({type: 'application/json'}) BEFORE any JSON parsing middleware:
```javascript
// CORRECT ORDER
app.post('/webhook', express.raw({type: 'application/json'}), handleWebhook);
app.use(express.json()); // General middleware after routes
```3. Request Body Already Stringified
Issue: You stringifiedreq.body before passing to constructEvent().
Fix: Pass raw body buffer directly—don't call JSON.stringify() or .toString().---
Version Uncertainty Note
Stripe SDK compatibility: This guide covers stripe@^14.0.0. If using older versions (pre-2022), timestamp tolerance behavior may differ. Check your package.json and run npm update stripe@latest if unsure. The signing mechanism has remained stable since 2019, but error messages changed slightly in v12+.
---
Related Resources
Found a different variation? Drop it in the comments below.