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 DevelopersWebhooks
  • 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