Stripe: webhook signature verification failing [2026 fix]

Webhook signature mismatch from endpoint secret or signing algorithm mismatch—validate secret against dashboard and use correct signing timestamp.

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

TL;DR

Cause: Your endpoint secret doesn't match the one Stripe is using, OR your code is signing the wrong request body/timestamp combination. Fix: Pull the EXACT secret from your Stripe dashboard (not from env file), ensure you're using the raw request body (not parsed JSON), and verify the timestamp parameter exists.

---

Real Console Error Messages

``` [ERROR] Error: No matching signing secret found. spect signature c7d34a2f9c8b1e2d3f4a5b6c7d8e9f0a1b2c3d4e against endpoint signing secret.

[ERROR] SignatureVerificationError: Unable to verify request signature. Most likely cause: Using request body after it has been read or converted to JSON.

[ERROR] Webhook signature verification failed: timestamp outside the tolerance window. Current time: 1703088000, Webhook timestamp: 1703087500, Tolerance: 300s

[ERROR] webhook endpoint returned an error: 401 sig_verification_failed Request headers missing 'stripe-signature' or malformed format.

[ERROR] HMAC signature mismatch. Expected: v1=a1b2c3d4e5f6..., Received: v1=z9y8x7w6v5u4... ```

---

The Problem: Broken vs Fixed Code

❌ BROKEN CODE (Common Mistakes)

```javascript // MISTAKE #1: Using parsed body instead of raw app.post('/webhook', express.json(), (req, res) => { const sig = req.headers['stripe-signature']; const body = req.body; // ❌ WRONG: Already parsed! try { const event = stripe.webhooks.constructEvent( body, // ❌ Should be RAW string, not object sig, process.env.STRIPE_WEBHOOK_SECRET ); } catch (err) { return res.status(400).send(Webhook Error: ${err.message}); } });

// MISTAKE #2: Wrong secret source const secret = process.env.STRIPE_WEBHOOK_SECRET; // ❌ Stale env var

// MISTAKE #3: Stripping timestamp from signature header const sig = req.headers['stripe-signature'].split(',')[0]; // ❌ Breaks verification ```

✅ FIXED CODE (Production Ready)

```javascript // FIX #1: Capture raw body BEFORE parsing app.post( '/webhook', express.raw({type: 'application/json'}), // ✅ Keep body as Buffer (req, res) => { const sig = req.headers['stripe-signature']; const body = req.body; // ✅ Raw buffer, not parsed try { const event = stripe.webhooks.constructEvent( body, // ✅ Pass raw Buffer/string sig, process.env.STRIPE_WEBHOOK_SECRET // ✅ Verify this matches dashboard ); // Handle specific events switch (event.type) { case 'payment_intent.succeeded': // Process payment break; } res.json({received: true}); } catch (err) { console.error('Webhook signature verification failed:', err.message); return res.status(400).send(Webhook Error: ${err.message}); } } );

// FIX #2: Verify secret against dashboard // Go to: Stripe Dashboard → Developers → Webhooks → Select endpoint // Copy the "Signing secret" (starts with 'whsec_') const WEBHOOK_SECRET = 'whsec_live_51A234567890abcdefghijk'; // ✅ From dashboard

// FIX #3: Keep full signature header intact const sig = req.headers['stripe-signature']; // ✅ Pass complete header ```

---

Step-by-Step Verification

1. Find your real secret: - Log into [Stripe Dashboard](https://dashboard.stripe.com) - Navigate: Developers → Webhooks - Click your endpoint - Copy "Signing secret" (whsec_*) - Update your .env file - Restart your server

2. Test the endpoint locally: ```bash # Use Stripe CLI to forward webhooks stripe listen --forward-to localhost:3000/webhook stripe trigger payment_intent.succeeded ``` If this works locally, your code is correct.

3. Verify request body handling: ```javascript // Debug: Log what you're receiving app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { console.log('Body type:', typeof req.body); console.log('Body is Buffer:', Buffer.isBuffer(req.body)); console.log('Signature:', req.headers['stripe-signature']); }); ```

---

Still Broken? Check These Too

1. Timestamp drift: Server clock skewed >5 minutes from actual time. Run date command; if off, sync with NTP.

2. Multiple webhook endpoints: You might be testing against a different endpoint. Verify the URL in dashboard matches your deployment.

3. Middleware ordering: If you have multiple express.json() calls, only the first one capturing raw body will work. Check middleware stack order in your Express setup. See [middleware guide](/guide=middleware-order) for details.

4. Stripe SDK version mismatch: We're uncertain if behavior changed in versions <2.0. Run npm list stripe and consult [Stripe Node.js docs](https://github.com/stripe/stripe-node) for your version.

5. Custom parsing: If you're using a body parser like Busboy or Multer before Stripe middleware, you've already consumed the body stream. See [request body](/guide=request-body-stream) guide.

---

Quick Checklist

  • [ ] Secret starts with whsec_ (not sk_)
  • [ ] Using express.raw() not express.json()
  • [ ] Passing req.body as Buffer, not object
  • [ ] Stripe CLI test passes: stripe trigger payment_intent.succeeded
  • [ ] Server time within ±5 minutes of actual time
  • [ ] Webhook endpoint URL matches dashboard exactly
  • ---

    Official Docs

  • [Stripe Webhook Signing Docs](https://stripe.com/docs/webhooks/signatures)
  • [stripe.webhooks.constructEvent()](https://stripe.com/docs/api/webhooks/construct_event)
  • [Stripe CLI Testing](https://stripe.com/docs/stripe-cli/webhooks)
  • 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