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