Stripe: webhook signature verification failing [2026 fix]

Webhook signature mismatch: timestamp too old or signing secret wrong. Verify endpoint secret matches live/test mode and clock skew < 5min.

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

TL;DR

Cause: Your webhook endpoint is using the wrong signing secret or your server's clock is skewed >5 minutes from Stripe's servers. Fix: Verify you're using the correct whsec_ or wh_secret_ for your environment (test vs. live) and sync your server time with NTP.

---

Exact Error Messages from Console

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

``` StripeSignatureVerificationError: Timestamp outside the tolerance window at verifySignatureHeader (/node_modules/stripe/lib/utils.js:156:12) ```

``` Error: Invalid signature: expected sig_xxxx but received sig_yyyy ```

``` Webhook endpoint failed: 403 Forbidden - signature verification failed for [evt_123abc] ```

``` stripe.error.SignatureVerificationError: Unexpected signed header format ```

---

Broken Code vs. Exact Fix

❌ BROKEN (Most Common)

```javascript // webhooks/stripe.js const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { // Using wrong secret or no secret at all const sig = req.headers['stripe-signature']; try { // WRONG: Using API key instead of webhook secret const event = stripe.webhooks.constructEvent( req.body, sig, process.env.STRIPE_SECRET_KEY // ← THIS IS THE BUG ); res.json({received: true}); } catch (err) { return res.status(400).send(Webhook Error: ${err.message}); } }); ```

✅ FIXED

```javascript // webhooks/stripe.js const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; // whsec_test_xxx or whsec_live_xxx

app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { const sig = req.headers['stripe-signature']; try { // CORRECT: Using webhook endpoint secret const event = stripe.webhooks.constructEvent( req.body, sig, endpointSecret // ← WEBHOOK SECRET, NOT API KEY ); // Process event switch (event.type) { case 'payment_intent.succeeded': handlePaymentSuccess(event.data.object); break; case 'charge.failed': handleChargeFailed(event.data.object); break; } res.json({received: true}); } catch (err) { console.error(Webhook signature verification failed: ${err.message}); return res.status(400).send(Webhook Error: ${err.message}); } }); ```

Critical Differences

| Aspect | Broken | Fixed | |--------|--------|-------| | Secret Source | STRIPE_SECRET_KEY (API key) | STRIPE_WEBHOOK_SECRET (webhook secret) | | Secret Format | sk_test_xxx or sk_live_xxx | whsec_test_xxx or whsec_live_xxx | | Where to Get | Developers → API Keys | Webhooks → Signing Secret | | Environment | Must match (test/live) | MUST match (test/live) |

---

Version-Specific Notes

  • stripe-node < 8.0: Uses older parameter names—verify you're on current version
  • Stripe API 2023-10-16+: No behavioral changes to signature verification in recent releases, but always confirm your STRIPE_WEBHOOK_SECRET format starts with whsec_
  • Python/Ruby/Go libraries: All use same verification logic—this guide's core fix applies across SDKs
  • ---

    Additional Diagnostics

    ```bash

    1. Verify your endpoint secret is loaded

    echo $STRIPE_WEBHOOK_SECRET

    Output should be: whsec_test_xxx or whsec_live_xxx

    2. Check server clock skew (signature tolerance is ±5 minutes)

    date

    Should match: https://worldtimeapi.org/api/timezone/Etc/UTC

    3. Confirm webhook is hitting correct environment

    Test webhook: use whsec_test_xxx

    Live webhook: use whsec_live_xxx

    ```

    ---

    Still Broken? Check These Too

    1. [Raw Body Parsing Issue](/?guide=express-raw-body) — Stripe requires unparsed Buffer body. If you're using express.json() middleware before webhook route, signature will fail. Move webhook route *before* global middleware or use express.raw({type: 'application/json'}).

    2. Test vs. Live Mode Mismatch — Dashboards often switch modes. Confirm your .env has whsec_test_xxx for development. Dashboard: Settings → Webhooks → click endpoint → copy signing secret.

    3. [Webhook Endpoint URL Wrong](/?guide=stripe-webhook-url) — Signature is tied to endpoint URL. If you changed from example.com/webhook to example.com/stripe-webhook, webhook was never re-sent. Re-trigger in Stripe Dashboard or wait for next event.

    ---

    Official Resources

    [Stripe Webhook Signatures Documentation](https://stripe.com/docs/webhooks/signatures)

    ---

    Found a different variation? Drop it in the comments below—every 2am horror story helps the next person.

    🔥 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