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