Stripe: webhook signature verification failing [2026 fix]

Webhook signature mismatch from incorrect endpoint secret or missing raw body. Verify STRIPE_WEBHOOK_SECRET matches dashboard and pass raw request body, not parsed JSON.

Stripe Webhook Signature Verification Failing

TL;DR

Cause: You're using the wrong webhook secret or passing parsed JSON instead of the raw request body to the verification function.

Fix: Copy the exact endpoint secret from Stripe Dashboard → Developers → Webhooks, and ensure your framework passes the raw body (not JSON-parsed) to stripe.Webhook.constructEvent().

---

Real Console Errors

Here are the exact errors you'll see at 2am:

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

``` StripeSignatureVerificationError: No signatures found matching the expected signature for payload. Webhook secret (***_test_abc123) may be invalid. ```

``` Unhandled rejection StripeSignatureVerificationError: Timestamp outside the tolerance window ```

``` Error: Unable to extract timestamp and signatures from header ```

``` 401 Unauthorized - Webhook endpoint signature verification failed ```

---

The Problem: Side-by-Side Code Comparison

❌ BROKEN CODE

```javascript // Express.js - WRONG app.post('/webhook', express.json(), (req, res) => { const sig = req.headers['stripe-signature']; const secret = process.env.STRIPE_WEBHOOK_SECRET; try { // BUG: req.body is already parsed JSON, not raw bytes const event = stripe.webhooks.constructEvent( req.body, // ❌ WRONG - this is a JavaScript object sig, secret ); res.json({received: true}); } catch (err) { res.status(400).send(Webhook Error: ${err.message}); } }); ```

✅ FIXED CODE

```javascript // Express.js - CORRECT app.post( '/webhook', express.raw({type: 'application/json'}), // ✅ Raw body middleware (req, res) => { const sig = req.headers['stripe-signature']; const secret = process.env.STRIPE_WEBHOOK_SECRET; try { // ✅ CORRECT - req.body is Buffer with raw bytes const event = stripe.webhooks.constructEvent( req.body, // Now this is the raw request body sig, secret ); res.json({received: true}); } catch (err) { res.status(400).send(Webhook Error: ${err.message}); } } ); ```

Additional Framework Examples

Next.js API Route - BROKEN: ```javascript export default async (req, res) => { const event = stripe.webhooks.constructEvent( JSON.stringify(req.body), // ❌ Double-stringifying causes signature mismatch req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET ); }; ```

Next.js API Route - FIXED: ```javascript export const config = { api: { bodyParser: false } // ✅ Disable auto-parsing };

export default async (req, res) => { const buf = await getRawBody(req); // Use raw-body library const event = stripe.webhooks.constructEvent( buf, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET ); }; ```

---

Root Causes Checklist

1. Wrong Webhook Secret: You copied the signing secret instead of the endpoint secret. In Stripe Dashboard, go Developers → Webhooks → click your endpoint → copy "Signing secret" (starts with whsec_)

2. Middleware Parsing Body: Express's express.json() or similar middleware automatically parses the raw body into a JavaScript object. Stripe's constructEvent() requires the original raw bytes to verify the signature. Use express.raw() instead.

3. Environment Variable Typo: Verify your .env file has STRIPE_WEBHOOK_SECRET=whsec_xxxxx (test mode secret should include _test_)

4. Using Live Secret in Test Environment: If you're testing with test webhooks but using your live signing secret (or vice versa), verification fails. Ensure environment matches.

5. Timestamp Tolerance: Stripe rejects webhooks older than 5 minutes. If your server clock is severely out of sync, synchronize with NTP.

---

Still broken? Check these too

  • [Body parser conflict - shared middleware guide](/?guide=express-middleware-order) - Other body parser middleware may interfere; ensure raw middleware runs first
  • [Environment variable loading issues](/?guide=dotenv-webpack) - process.env.STRIPE_WEBHOOK_SECRET might be undefined; add explicit logging before constructEvent()
  • [Stripe SDK version mismatch](/?guide=stripe-sdk-versions) - Method signatures changed in Stripe SDK v9+; verify your installed version matches docs
  • ---

    Debug Steps

    ```javascript // Add temporary logging (remove after debugging) console.log('Secret:', process.env.STRIPE_WEBHOOK_SECRET?.slice(-8)); console.log('Signature header:', req.headers['stripe-signature']?.slice(0, 20)); console.log('Body type:', typeof req.body, 'Is Buffer:', Buffer.isBuffer(req.body)); ```

    If Body type: object appears, you're parsing too early. Remove the JSON parser before the webhook route.

    ---

    Official Resources

  • [Stripe Webhook Documentation](https://stripe.com/docs/webhooks)
  • [Stripe Node.js Library - Constructing Events](https://stripe.com/docs/webhooks/signatures)
  • ---

    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