Stripe: webhook signature verification failing [2026 fix]
Webhook signature mismatch caused by endpoint secret misconfiguration or timestamp skew; verify your signing secret and clock sync.
Stripe: Webhook Signature Verification Failing [2am Emergency Fix]
TL;DR
Cause: Your endpoint is using the wrong webhook signing secret or your server's clock is skewed by >5 minutes. Fix: VerifySTRIPE_WEBHOOK_SECRET env var matches your dashboard webhook endpoint, and sync your server time with NTP.---
Real Console Error Messages
Here are the exact errors you'll see:
``` Error: No signatures found matching the expected signature for payload. Attempted to construct signature using the following secret: 'whsec_live_***' but no signatures found in the 'stripe-signature' header. ```
``` StripeSignatureVerificationError: No valid signature found. ```
``` Webhook error: Unable to extract timestamp and signatures from header. Header value: undefined ```
``` Error: Timestamp outside the tolerance window: received 1704067200, current 1704067920 (timestamp difference: 720 seconds exceeds max tolerance of 300 seconds) ```
``` Signature verification failed. Expected sig: sha256=abc123..., got: sha256=xyz789... ```
---
The Problem: Side-by-Side Code Fix
❌ BROKEN CODE
```javascript // webhook.js - WRONG const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const express = require('express'); const app = express();
app.post('/webhook', express.json(), (req, res) => { // WRONG: Using parsed JSON body directly const event = req.body; // WRONG: Not verifying signature at all if (event.type === 'payment_intent.succeeded') { console.log('Payment successful:', event.data.object.id); res.json({received: true}); } }); ```
✅ CORRECT CODE
```javascript // webhook.js - CORRECT const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const express = require('express'); const app = express();
// CRITICAL: Must be raw body, NOT parsed JSON
app.post('/webhook',
express.raw({type: 'application/json'}), // Raw buffer required
(req, res) => {
const sig = req.headers['stripe-signature'];
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; // whsec_live_***
let event;
try {
// Signature verification happens HERE
event = stripe.webhooks.constructEvent(
req.body, // Raw body buffer
sig, // Signature from header
webhookSecret // Your endpoint's signing secret
);
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).send(Webhook Error: ${err.message});
}
// Only process AFTER verification passes
if (event.type === 'payment_intent.succeeded') {
console.log('Payment successful:', event.data.object.id);
}
res.json({received: true});
}
);
```
Key differences:
express.raw() instead of express.json() — the signature is computed on the raw bytesstripe.webhooks.constructEvent() verifies the signature before parsing---
Verification Checklist (Do This Now)
1. Verify Your Webhook Secret
Go to [Stripe Dashboard → Developers → Webhooks](https://dashboard.stripe.com/webhooks). Find your endpoint:
```bash
Should match exactly
echo $STRIPE_WEBHOOK_SECRETShould output: whsec_live_4eC39HqLyjWDarhtT1g3bZ8y or whsec_test_...
```If it says pk_live_... or sk_live_..., you copied the wrong key. Copy the Signing secret under "Endpoint details."
2. Check Server Clock Skew
Stripe rejects signatures older than 5 minutes. Run:
```bash
Linux/Mac
date && curl -s https://worldtimeapi.org/api/timezone/Etc/UTC | grep unixtimeIf difference > 300 seconds, sync your clock
sudo ntpdate -s time.nist.gov # or use timedatectl on systemd ```3. Validate Raw Body Handling
Add this debug endpoint temporarily:
```javascript app.post('/webhook-debug', express.raw({type: 'application/json'}), (req, res) => { console.log('Body type:', typeof req.body); console.log('Body is Buffer:', Buffer.isBuffer(req.body)); console.log('Signature header:', req.headers['stripe-signature']); res.json({ok: true}); }); ```
Test with Stripe CLI:
```bash stripe listen --forward-to localhost:3000/webhook-debug stripe trigger payment_intent.succeeded ```
---
Still Broken? Check These Too
1. Multiple middleware parsing JSON: If you have app.use(express.json()) globally, it parses the body before your raw middleware sees it. Move webhook routes BEFORE global JSON middleware, or skip JSON parsing for /webhook* routes.
2. Ngrok/proxy stripping the signature header: Some reverse proxies drop custom headers. Verify headers are passing through: curl -v https://your-tunnel.ngrok.io/webhook and check response includes stripe-signature.
3. Environment variable not loaded: In Node.js, process.env is read at startup. If you added STRIPE_WEBHOOK_SECRET to .env after starting the server, restart: node app.js (not just saving the file).
---
Related Guides
---
Official Documentation
[Stripe Webhook Signature Verification](https://stripe.com/docs/webhooks/signatures) [Stripe Node.js Library Reference](https://stripe.com/docs/api/libraries)
---
Found a different variation? Drop it in the comments below.