Stripe: webhook signature verification failing [2026 fix]
Webhook signature verification fails when endpoint secret is missing, malformed, or timestamp validation rejects old requests—add correct secret and check system time.
Stripe: Webhook Signature Verification Failing [2am Emergency Fix]
TL;DR
Cause: Your endpoint secret is missing, wrong, or your server's system clock is skewed beyond Stripe's 5-minute tolerance window. Fix: Verify theSTRIPE_WEBHOOK_SECRET environment variable matches your Stripe dashboard endpoint, and sync your server time with NTP.---
Exact Error Messages You're Seeing
``` Error: No signatures found matching the expected signature for payload. ```
``` WebhookSignatureVerificationError: Timestamp outside the tolerance window ```
``` StripeSignatureVerificationError: Unable to extract timestamp and signatures from header ```
``` No API key provided. (If you need help configuring Stripe, visit https://support.stripe.com/contact/email) ```
``` Invalid signature: signature does not match payload ```
---
The Problem: Broken vs. Fixed Code
❌ BROKEN CODE
```javascript // Express.js webhook endpoint const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/webhook', express.json(), (req, res) => { // Missing signature verification! const event = req.body; if (event.type === 'charge.succeeded') { console.log('Charge succeeded:', event.data.object); } res.json({received: true}); }); ```
✅ FIXED CODE
```javascript // Express.js webhook endpoint with proper verification const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
// CRITICAL: Use raw body, not parsed JSON for signature verification app.post( '/webhook', express.raw({type: 'application/json'}), (req, res) => { const sig = req.headers['stripe-signature']; let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
endpointSecret
);
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).send(Webhook Error: ${err.message});
}
if (event.type === 'charge.succeeded') { console.log('Charge succeeded:', event.data.object); }
res.json({received: true}); } ); ```
Key Differences
| Issue | Broken | Fixed |
|-------|--------|-------|
| Body parsing | express.json() (parsed) | express.raw() (raw bytes) |
| Signature header | Not read | req.headers['stripe-signature'] |
| Verification call | Missing | stripe.webhooks.constructEvent() |
| Error handling | None | Try-catch with proper HTTP 400 |
| Endpoint secret | Not used | Loaded from process.env |
---
Step-by-Step 2am Debugging
1. Verify Environment Variable Exists
```bash
SSH into your production server
echo $STRIPE_WEBHOOK_SECRETShould output something like: whsec_live_...
If empty, you found the problem
```2. Check Stripe Dashboard Match
Go to: Stripe Dashboard → Developers → Webhooks → Select your endpoint → Reveal signing secret. Copy the exact value and verify it matches your env var character-for-character.
3. Sync Server Time (Critical!)
Stripe rejects timestamps outside ±5 minutes of current UTC.
```bash
Check current system time
date -uSync with NTP (Ubuntu/Debian)
sudo ntpdate -s time.nist.govOr use timedatectl (systemd systems)
sudo timedatectl set-ntp true sudo timedatectl status ```4. Test Webhook Locally
Use Stripe's CLI to send test events:
```bash stripe listen --forward-to localhost:3000/webhook
Copy the signing secret it shows
Update STRIPE_WEBHOOK_SECRET in your .env
In another terminal:
stripe trigger payment_intent.succeeded ```---
Still Broken? Check These Too
Issue 1: Middleware Order Matters
If you're using body-parser or other middleware, ensure raw Stripe routes load *before* JSON parsing middleware:```javascript // CORRECT order app.post('/webhook', express.raw({type: 'application/json'}), handler); app.use(express.json()); // After webhook
// WRONG order app.use(express.json()); app.post('/webhook', ...); // Will receive parsed body, signature fails ```
Issue 2: Webhook Secret vs. API Key Confusion
STRIPE_SECRET_KEY: For API calls (charges, customers, etc.)STRIPE_WEBHOOK_SECRET: For webhook verification onlyThey are different values. Check you're using the webhook secret in constructEvent().
Issue 3: Multiple Webhook Endpoints
If you have multiple endpoints (test + live), verify you're using the correct secret for each environment. Test vs. Live signing secrets are different prefixes (whsec_test_ vs whsec_live_).---
Related Guides
---
Official Resources
---
Version Note: This guide covers Stripe Node.js SDK v11.0.0+. Earlier versions use stripe.webhooks.constructEventAsync(). Check your package.json version if uncertain.
Found a different variation? Drop it in the comments.