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