Stripe: webhook signature verification failing [2026 fix]

Webhook signature mismatch caused by endpoint secret mismatch or missing raw body. Verify STRIPE_WEBHOOK_SECRET matches dashboard and use raw request body.

Stripe: Webhook Signature Verification Failing [2026 Fix]

TL;DR

Cause: Your endpoint is using the wrong webhook secret or passing JSON-parsed body instead of raw request body to verification. Fix: Match your STRIPE_WEBHOOK_SECRET to Stripe Dashboard > Webhooks > Signing Secret, and verify you're passing req.rawBody (not req.body) to stripe.webhooks.constructEvent().

---

Exact Console Error Messages

``` Error: No signatures found matching the expected signature for payload. at Webhook.constructEvent (/node_modules/stripe/lib/resources/Webhooks.js:45:12) ```

``` Stripe signature verification failed: error decoding body at verifyWebhookSignature (webhook.js:23:15) ```

``` No match found for signature header 't=1704067200,v1=abcd1234...' error: "Unable to extract timestamp and signatures from header" ```

``` Error: Signature timestamp outside the tolerance window tolerance: 300 seconds timestamp: 1704067200 ```

``` TypeError: Cannot read property 'split' of undefined at constructEvent (stripe/webhooks.js:51:8) ```

---

Code Comparison: Broken vs. Fixed

❌ BROKEN (Express.js example)

```javascript // webhook.js - WRONG const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const express = require('express'); const app = express();

app.use(express.json()); // This parses body - PROBLEM!

app.post('/webhook', (req, res) => { const sig = req.headers['stripe-signature']; try { // req.body is already parsed JSON, not raw bytes const event = stripe.webhooks.constructEvent( req.body, // WRONG - this is parsed object sig, process.env.STRIPE_WEBHOOK_SECRET ); res.json({ received: true }); } catch (err) { console.error('Webhook error:', err.message); res.status(400).send(Webhook Error: ${err.message}); } }); ```

✅ FIXED (Express.js example)

```javascript // webhook.js - CORRECT const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const express = require('express'); const app = express();

// CRITICAL: Parse raw body ONLY for webhook endpoint app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['stripe-signature']; try { // req.body is now Buffer (raw bytes) - CORRECT const event = stripe.webhooks.constructEvent( req.body, // Now this is raw bytes, not parsed sig, process.env.STRIPE_WEBHOOK_SECRET // Must match Stripe Dashboard ); // Handle event type switch (event.type) { case 'payment_intent.succeeded': console.log('Payment successful:', event.data.object.id); break; case 'charge.failed': console.log('Payment failed:', event.data.object.id); break; } res.json({ received: true }); } catch (err) { console.error('Webhook error:', err.message); res.status(400).send(Webhook Error: ${err.message}); } });

// Other routes can use normal JSON parser app.use(express.json()); app.post('/api/other', (req, res) => { // normal body parsing here }); ```

---

Verification Checklist

1. Webhook Secret Mismatch (Most Common)

  • Go to [Stripe Dashboard](https://dashboard.stripe.com/webhooks) > Webhooks
  • Find your endpoint URL
  • Copy the "Signing secret" (starts with whsec_)
  • Add to .env: STRIPE_WEBHOOK_SECRET=whsec_xxxx
  • Confirm it matches your code's process.env.STRIPE_WEBHOOK_SECRET
  • 2. Raw Body Requirement

  • Stripe's signature verification requires the exact raw HTTP body as bytes
  • If you parse to JSON first, the bytes change and signature fails
  • Use express.raw() or equivalent for that route only
  • I'm uncertain about differences in Stripe SDK versions before 11.x—check your package.json version
  • 3. Timestamp Tolerance

  • By default, signatures older than 5 minutes are rejected
  • Your server clock must be accurate (use NTP sync)
  • For testing, Stripe CLI auto-signs with current timestamp
  • ---

    Still broken? Check these too

    1. Environment Variable Not Loading — Verify console.log(process.env.STRIPE_WEBHOOK_SECRET) outputs the correct secret (not undefined). Restart your server after changing .env.

    2. Multiple Webhooks with Different Secrets — If you registered multiple endpoints in Stripe Dashboard, each has a unique signing secret. Confirm you're using the secret for *this specific* endpoint URL.

    3. Test vs. Live Secret Mismatch — Are you testing in live mode but using a test-mode signing secret? Dashboard has separate signing secrets for test and live modes—toggle the mode switch in top-left corner.

    ---

    Related Guides

  • [Stripe Payment Intent failures - handling declined cards](/?guide=stripe-payment-intent)
  • [Setting up Stripe webhook retries and dead-letter queues](/?guide=webhook-resilience)
  • ---

    Official Documentation

    📖 [Stripe Webhook Signature Verification](https://stripe.com/docs/webhooks/signatures) 📖 [Stripe.js Webhooks API Reference](https://github.com/stripe/stripe-node#webhook-signing)

    ---

    Found a different variation? Drop it in the comments.

    🔥 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