Stripe: webhook signature verification failing [2026 fix]

Webhook signature mismatch caused by endpoint secret misconfiguration or timestamp drift—verify secret matches dashboard and check server time sync.

Stripe: Webhook Signature Verification Failing [2026 Fix]

TL;DR

Cause: Your endpoint secret doesn't match Stripe's dashboard, or your server's system clock is >5 minutes out of sync. Fix: Copy the *exact* signing secret from Stripe Dashboard → Webhooks → [Your Endpoint] → Reveal, paste into your .env, restart your app, and verify ntpdate -u pool.ntp.org on your server.

---

Real Console Error Messages

``` Error: No signatures found matching the expected signature for payload. Received signature(s): t=1704067234,v1=abcd1234... ```

``` Stripe.error.SignatureVerificationError: No signatures found matching the expected signature for payload at VerifySignatureHeader (/node_modules/@stripe/stripe-js/lib/webhook.js:142:15) ```

``` wt_signature=t=1704067234,v1=xyz789; expected=t=1704067234,v1=abc123 [MISMATCH] ```

``` Error: Timestamp outside the tolerance window. received: 1704067234, now: 1704067534, tolerance: 300 ```

``` SignatureVerificationError: Unable to extract timestamp and signatures from header 'stripe-signature' ```

---

Broken vs. Fixed Code

❌ BROKEN: Missing or Wrong Secret

```javascript const stripe = require('stripe')('sk_live_...');

app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { // Using hardcoded/wrong secret or undefined env var const endpointSecret = 'whsec_1234567890abcdef'; // ← WRONG! Copy-pasted once, now stale let event; try { event = stripe.webhooks.constructEvent( req.body, req.headers['stripe-signature'], endpointSecret // ← Stripe rejects because this doesn't match their records ); } catch (err) { return res.status(400).send(Webhook Error: ${err.message}); } }); ```

✅ FIXED: Correct Secret + Time Sync

```javascript const stripe = require('stripe')('sk_live_...');

app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { // Pull EXACT secret from Stripe Dashboard → Webhooks → [Endpoint Name] → Reveal const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; if (!endpointSecret) { console.error('STRIPE_WEBHOOK_SECRET not set in environment'); return res.status(500).send('Webhook handler misconfigured'); } let event; try { event = stripe.webhooks.constructEvent( req.body, req.headers['stripe-signature'], endpointSecret // ← Matches Stripe's records exactly ); } catch (err) { console.error('Webhook signature verification failed:', err.message); return res.status(400).send(Webhook Error: ${err.message}); } // Process event safely res.json({received: true}); }); ```

What changed: 1. Load secret from environment variable (not hardcoded) 2. Validate the secret exists before using it 3. Copy secret *directly* from Stripe Dashboard → don't manually type it 4. Ensure your server's clock is synced (see "Still broken?" below)

---

Step-by-Step Verification

1. Verify Your Endpoint Secret (Critical)

  • Log into [Stripe Dashboard](https://dashboard.stripe.com/)
  • Navigate to Developers → Webhooks
  • Click your endpoint URL
  • Click Reveal next to "Signing secret"
  • Copy the entire value starting with whsec_
  • Paste into your .env file: STRIPE_WEBHOOK_SECRET=whsec_...
  • Do not manually type this—copy/paste only
  • 2. Confirm Environment Variable Loading

    ```javascript console.log('Secret length:', process.env.STRIPE_WEBHOOK_SECRET?.length); console.log('First 10 chars:', process.env.STRIPE_WEBHOOK_SECRET?.substring(0, 10)); // Should show: whsec_ ```

    3. Check Server Time Sync

    ```bash

    On Linux/macOS

    ntpdate -u pool.ntp.org

    Check current time

    date

    On Docker/containers, restart to inherit host time

    docker-compose down && docker-compose up -d ```

    Stripe rejects signatures older than 5 minutes. If your server is >5 min behind, all webhooks fail.

    ---

    Still broken? Check these too

    1. Webhook Endpoint URL Mismatch

    - Stripe sends to the exact URL registered in Dashboard - If you changed your domain/port, update the endpoint URL in Dashboard → Webhooks - Test delivery shows 400 Webhook handler misconfigured → check URL is reachable

    2. Body Parsing Middleware Conflict

    - If you use express.json() *before* the webhook route, Stripe's raw body gets consumed - Solution: Register webhook route *before* global JSON middleware, or move to separate router - See [related webhook routing guide](/?guide=express-middleware-order)

    3. Multiple Endpoint Secrets (Development vs. Production)

    - Stripe generates different whsec_ for test and live modes - Confirm you're using the secret from the *correct* mode (test/live toggle in Dashboard) - Mismatched mode = signature always fails - See [related test/live mode guide](/?guide=stripe-test-live-setup)

    ---

    Uncertainty Callout

    ⚠️ Version Note: This guide covers @stripe/stripe-js v2.x and stripe Node package v10+. If you're on v8.x or earlier, the webhook signature format differs slightly—[check official docs](https://stripe.com/docs/webhooks/signatures) for your version's exact implementation.

    ---

    Official Resources

  • [Stripe Webhook Signature Verification Docs](https://stripe.com/docs/webhooks/signatures)
  • [Stripe Dashboard Webhooks](https://dashboard.stripe.com/webhooks)
  • ---

    Found a different variation? Drop it in the comments

    Escalating to production at 2am? Missing edge case? Share your exact error message and environment setup below—we'll add it to this guide.

    🔥 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