Stripe: webhook signature verification failing [2026 fix]

Webhook signature mismatch—verify endpoint secret, check raw body usage, ensure no JSON parsing before validation.

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

TL;DR

Cause: Your code is validating the signature AFTER parsing the request body to JSON, or using the wrong endpoint secret. Fix: Always validate the raw request body BEFORE parsing, and confirm your endpoint secret matches the webhook endpoint in Stripe Dashboard.

---

Real Console Error Messages

Here are exact errors you'll see when this breaks:

``` Error: No signatures found matching the expected signature for payload. ```

``` StripeSignatureVerificationError: Unable to verify webhook signature Expected sig: t=1234567890,v1=abc123... ```

``` Error: Webhook Error: Invalid signature. Webhook secret does not match expected value ```

``` TypeError: Cannot read property 'id' of undefined at verifyWebhookSignature ```

``` StripeInvalidSignatureError: No signatures found matching the expected signature for payload. Received signature(s): [] ```

---

The Broken vs. Fixed Code

❌ BROKEN: Parsing Before Validation

```javascript // Express.js example - WRONG app.post('/webhook', express.json(), (req, res) => { // ⚠️ PROBLEM: Body is already parsed to JSON object const sig = req.headers['stripe-signature']; try { const event = stripe.webhooks.constructEvent( req.body, // ❌ This is an object, not raw string sig, process.env.STRIPE_ENDPOINT_SECRET ); res.json({ received: true }); } catch (err) { res.status(400).send(Webhook Error: ${err.message}); } }); ```

✅ FIXED: Validate Raw Body First

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

app.post( '/webhook', express.raw({ type: 'application/json' }), // ✅ Keep as Buffer (req, res) => { const sig = req.headers['stripe-signature']; try { const event = stripe.webhooks.constructEvent( req.body, // ✅ Now a Buffer/raw string sig, process.env.STRIPE_ENDPOINT_SECRET // ✅ Verify this matches Dashboard ); // Only parse AFTER validation console.log('Verified event:', event.type); res.json({ received: true }); } catch (err) { console.error('Webhook error:', err.message); res.status(400).send(Webhook Error: ${err.message}); } } ); ```

❌ BROKEN: Wrong Endpoint Secret

```javascript const event = stripe.webhooks.constructEvent( req.body, sig, 'whsec_live_wrong1234567890' // ❌ Copy-pasted from old endpoint ); ```

✅ FIXED: Correct Secret from Environment

```javascript const event = stripe.webhooks.constructEvent( req.body, sig, process.env.STRIPE_ENDPOINT_SECRET // ✅ whsec_live_abc123... ); ```

---

Critical Checklist

1. Middleware Order Matters: If using Express, place webhook route BEFORE express.json() middleware: ```javascript app.post('/webhook', express.raw({type: 'application/json'}), handler); app.use(express.json()); // Other routes after ```

2. Endpoint Secret Match: - Go to Stripe Dashboard → Developers → Webhooks - Click your endpoint (e.g., https://yourdomain.com/webhook) - Copy the Signing secret (starts with whsec_) - Verify it matches process.env.STRIPE_ENDPOINT_SECRET

3. Environment Variable Typo: ```bash # Check exact name - case sensitive echo $STRIPE_ENDPOINT_SECRET ```

---

Still Broken? Check These Too

1. Reverse Proxy or Load Balancer: If behind nginx/AWS ALB, request body might be re-encoded. Check raw logs with req.rawBody or use middleware like bodyParser.raw() with proper configuration.

2. Multiple Webhook Endpoints: Stripe Dashboard shows all endpoints. You might be testing against a test mode endpoint while using your live secret—or vice versa. Confirm webhook is active (green checkmark) and matches your URL exactly.

3. Timestamp Validation: Stripe includes a timestamp in the signature (tolerance ±5 minutes). If server time is severely skewed, validation fails. Run date on production and sync NTP if off by >5 minutes.

---

Framework-Specific Notes

Next.js API Routes: Use req.body as Buffer if configured correctly, but most next defaults parse JSON. Solution: [webhook-signature-verification guide](/?guide=nextjs-stripe).

FastAPI/Python: Ensure you read the raw body before parsing: raw_body = await request.body() then pass to stripe.Webhook.construct_event(raw_body, sig_header, secret).

Uncertainty Flag: Version-specific behavior varies significantly between stripe-node v8 vs v10+. If using stripe@^10, the constructEvent() method has different buffer handling than v8. Check your package.json and test locally.

---

Resources

  • [Official Stripe Webhook Verification Docs](https://stripe.com/docs/webhooks/signatures)
  • [Related: Webhook Testing Locally](/?guide=stripe-testing-local)
  • Production checklist: [Stripe API Errors](/?guide=stripe-errors)
  • ---

    Found a different variation? Drop it in the comments — webhook issues are environment-specific and your edge case helps others at 2am.

    🔥 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