Stripe: webhook signature verification failing [2026 fix]

Webhook signature invalid due to raw body tampering or endpoint secret mismatch—use raw request body and verify secret environment variable.

Stripe: Webhook Signature Verification Failing [2am Fix]

TL;DR

Cause: You're passing parsed JSON instead of the raw request body to Stripe's signature verification, OR your STRIPE_WEBHOOK_SECRET is wrong/missing. Fix: Use the raw unparsed request body and confirm your endpoint secret matches your webhook configuration in the Stripe Dashboard.

---

Exact Console Errors You're Seeing

``` 1. Error: No signatures found matching the expected signature for payload 2. Webhook signature verification failed: Invalid signature 3. TypeError: Cannot read property 'id' of undefined at verifySignatureHeader 4. Invalid signature: expected <sig>, got <sig> 5. Webhook endpoint failed: 403 Forbidden (signature mismatch) ```

---

Broken Code vs. Exact Fix

❌ BROKEN (Express.js)

```javascript const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

app.post('/webhook', express.json(), (req, res) => { const sig = req.headers['stripe-signature']; try { // WRONG: express.json() already parsed the body const event = stripe.webhooks.constructEvent( JSON.stringify(req.body), // ❌ Re-stringifying loses original sig, process.env.STRIPE_WEBHOOK_SECRET ); res.json({received: true}); } catch (err) { res.status(400).send(Webhook Error: ${err.message}); } }); ```

✅ FIXED (Express.js)

```javascript const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

// CRITICAL: Raw body middleware BEFORE express.json() app.post('/webhook', express.raw({type: 'application/json'}), // ✅ Raw bytes preserved (req, res) => { const sig = req.headers['stripe-signature']; try { // CORRECT: Pass raw buffer directly const event = stripe.webhooks.constructEvent( req.body, // ✅ Raw body, not re-stringified sig, process.env.STRIPE_WEBHOOK_SECRET // ✅ Verify it exists ); console.log('✓ Webhook verified:', event.type); res.json({received: true}); } catch (err) { console.error('Webhook Error:', err.message); res.status(400).send(Webhook Error: ${err.message}); } } ); ```

❌ BROKEN (Next.js API Routes)

```javascript export default async function handler(req, res) { const sig = req.headers['stripe-signature']; try { const event = stripe.webhooks.constructEvent( JSON.stringify(req.body), // ❌ Already parsed by Next.js sig, process.env.STRIPE_WEBHOOK_SECRET ); } catch (err) { res.status(400).json({error: err.message}); } } ```

✅ FIXED (Next.js API Routes)

```javascript import { buffer } from 'micro';

export const config = { api: { bodyParser: false, // ✅ Disable automatic parsing }, };

export default async function handler(req, res) { const sig = req.headers['stripe-signature']; const buf = await buffer(req); // ✅ Get raw buffer try { const event = stripe.webhooks.constructEvent( buf, // ✅ Raw buffer sig, process.env.STRIPE_WEBHOOK_SECRET ); res.status(200).json({received: true}); } catch (err) { res.status(400).json({error: err.message}); } } ```

---

Critical Checklist

1. Endpoint Secret Match: In Stripe Dashboard → Developers → Webhooks → Click your endpoint → Copy "Signing secret" and paste into .env 2. Raw Body Middleware Order: Stripe verification middleware MUST come BEFORE express.json() or any body parsing 3. Environment Variable Exists: Run echo $STRIPE_WEBHOOK_SECRET in your deployment environment—if empty, restart your app after updating .env 4. Webhook Testing: Use Stripe CLI to test locally: ```bash stripe listen --forward-to localhost:3000/webhook stripe trigger payment_intent.succeeded ```

---

Still Broken? Check These Too

1. Timestamp Skew: Stripe rejects signatures older than 5 minutes. If your server clock is wrong, it will fail. Run ntpdate -s time.nist.gov on Linux or sync time on your server.

2. Multiple Webhook Secrets: You have separate secrets for Test vs. Live mode. Verify you're using the correct one for your current environment—test webhooks won't verify with your live secret.

3. Payload Modification in Transit: Proxies, load balancers, or middleware modifying the request body (e.g., logging middleware that reads the stream) will break the signature. Use a raw body capture that doesn't consume the stream twice. See [webhook debugging guide](/?guide=stripe-webhook-logging).

---

Version Note

⚠️ Stripe.js library behavior: We're referencing stripe.webhooks.constructEvent() which works in stripe@8.0.0+. If you're on stripe@6.x, the method is stripe.webhooks.constructEventAsync() and requires await. Confirm your installed version with npm list stripe.

---

Official Resources

  • [Stripe Webhooks Documentation](https://stripe.com/docs/webhooks/signatures)
  • [Stripe CLI Reference](https://stripe.com/docs/stripe-cli)
  • ---

    Found a different variation? Drop it in the comments—especially if you hit this with Flask, Django, or non-Express frameworks.

    Related: [Firebase Cloud Functions + Stripe webhooks](/?guide=firebase-stripe) | [Local webhook testing without ngrok](/?guide=stripe-local-testing)

    🔥 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