Stripe: webhook signature verification failing [2026 fix]

Webhook signature mismatch from timestamp drift or missing secret—regenerate endpoint key and verify clock sync.

Stripe: Webhook Signature Verification Failing [2am Emergency Fix]

TL;DR

Cause: Your webhook endpoint secret is stale, your server clock is drifting >5 minutes, or you're reading the raw body after Express already parsed it. Fix: Regenerate the endpoint signing secret in Stripe Dashboard, sync your system time with ntpdate, and ensure you're using the raw request body before any JSON parsing middleware.

---

Exact Error Messages You'll See

``` Error: No signatures found matching the expected signature for payload. ```

``` StripeSignatureVerificationError: Unable to extract timestamp and signatures from header 't=..., v1=...' ```

``` Webhook signature verification failed: Timestamp outside the tolerance window ```

``` Error: Invalid signature: signature version v1 not found in header ```

``` ReferenceError: Cannot read property 'raw' of undefined at verifySignature ```

---

The Problem: Broken vs Fixed Code

❌ BROKEN CODE (Common Mistake #1: Parsed Body)

```javascript // Express app.js - WRONG app.use(express.json()); // This parses the body!

app.post('/webhook', (req, res) => { const sig = req.headers['stripe-signature']; const secret = process.env.STRIPE_WEBHOOK_SECRET; try { // req.body is now a parsed object, NOT raw bytes const event = stripe.webhooks.constructEvent( req.body, // ❌ WRONG: Already stringified/parsed sig, secret ); } catch (err) { res.status(400).send(Webhook Error: ${err.message}); } }); ```

✅ FIXED CODE #1: Raw Body Before Parsing

```javascript // Express app.js - CORRECT app.use(express.json()); // Normal middleware

// THIS MUST COME BEFORE express.json() app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['stripe-signature']; const secret = process.env.STRIPE_WEBHOOK_SECRET; try { // req.body is raw Buffer here const event = stripe.webhooks.constructEvent( req.body, // ✅ CORRECT: Raw bytes sig, secret ); // Process event... res.json({ received: true }); } catch (err) { res.status(400).send(Webhook Error: ${err.message}); } } ); ```

❌ BROKEN CODE (Common Mistake #2: Stale Secret)

```javascript const stripe = require('stripe')(process.env.STRIPE_KEY);

// Secret loaded once at startup, never rotated const WEBHOOK_SECRET = 'whsec_live_abc123...'; // Hardcoded or from .env loaded 30 days ago ```

✅ FIXED CODE #2: Refresh & Verify Secret

```javascript const stripe = require('stripe')(process.env.STRIPE_KEY);

// 1. Go to Stripe Dashboard → Developers → Webhooks → Click endpoint // 2. Scroll to "Signing secret" → Click "Reveal" → Copy fresh value const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET; // Updated today

// Add this to your startup logs console.log('Webhook secret loaded (first 8 chars): whsec_' + WEBHOOK_SECRET.substring(7, 15) + '...');

// Verify it matches what Stripe has if (!WEBHOOK_SECRET || !WEBHOOK_SECRET.startsWith('whsec_')) { throw new Error('STRIPE_WEBHOOK_SECRET not set or invalid format'); } ```

❌ BROKEN CODE (Common Mistake #3: Clock Drift)

```javascript // No timestamp validation—server clock is 10 minutes behind const event = stripe.webhooks.constructEvent( req.body, sig, secret // Default tolerance: 5 minutes. If server clock drifts >5min, fails. ); ```

✅ FIXED CODE #3: Sync Time & Add Tolerance (Last Resort)

```bash

On your server - fix the root cause

sudo ntpdate -s time.nist.gov # Linux/Mac

or

sudo timedatectl set-ntp true # Systemd systems ```

```javascript // Only as temporary workaround while fixing clock const event = stripe.webhooks.constructEvent( req.body, sig, secret, 600 // 10-minute tolerance (default is 300 = 5 min) // ⚠️ This is a bandaid—fix your system clock! ); ```

---

Immediate Checklist (Do These NOW)

1. Regenerate secret: Dashboard → Developers → Webhooks → [your endpoint] → "Signing secret" → Reveal → Copy 2. Update .env: Paste fresh secret, restart app 3. Check server time: Run date command—should match date -u (within seconds) 4. Verify middleware order: Ensure express.raw() middleware loads BEFORE express.json() for webhook route 5. Check logs: Look for timestamp mismatch errors: Timestamp outside the tolerance window

---

Still Broken? Check These Too

1. [Webhook endpoint not receiving requests at all](/?guide=stripe-webhook-not-firing) — Different root cause, but overlapping troubleshooting 2. [Stripe API version mismatch in your webhook](/?guide=stripe-api-version-errors) — Can cause signature issues on version updates 3. Database/environment issues preventing secret load — Verify console.log(process.env.STRIPE_WEBHOOK_SECRET) outputs correct value

---

Version Notes

Stripe SDK: As of v15+ (2024+), behavior is consistent. If using v12 or earlier, I'm uncertain about edge cases with Buffer handling—update to current version if possible.

Node.js: Express middleware order matters on all versions. This guide applies to Express 4.x+.

---

Official Resources

[Stripe Webhooks Security Documentation](https://stripe.com/docs/webhooks/signatures)

[Express Raw Body Middleware](https://expressjs.com/en/api/express.html#express.raw)

---

Found a different variation? Drop it in the comments.

🔥 0d
LIVE
↓ PlanetScale rage spiking •⚠ Vercel pricing complaints •↑ Railway gaining fast •↑ Supabase happiness rising •↑ Resend loved by devs •↓ PlanetScale rage spiking •⚠ Vercel pricing complaints •↑ Railway gaining fast •↑ Supabase happiness rising
DEVELOPER PAIN RADAR // Loading...

Developers complain.
Opportunities appear.

We track what developers are struggling with today — and what opportunities that creates.

—
guides today
—
avg happiness
🔥 Pain
📖 Guides
🔭 Explore
👤 Mine
🔥 Pain Radar — rage scores today
↗ share
💡 Opportunity Feed — pain = market gap
📈 Tool Momentum
all scores →
📖 Latest Guide
all guides →
📖 All Guides
📊 Tool Scores
+ Submit
📰 Hacker News
➕ Submit a Tool
← back