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. Use console.log(req.originalUrl) to confirm which endpoint received the request.

2. Middleware Order Problem

Issue: If you're using express.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 stringified req.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

  • [Webhook signature verification deep-dive](/?guide=stripe-webhook-security)
  • [Handling clock skew in distributed systems](/?guide=ntp-clock-sync)
  • [Official Stripe Webhook Docs](https://stripe.com/docs/webhooks/signatures)
  • Found a different variation? Drop it in the comments below.

    🔥 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