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