Stripe: webhook signature verification failing [2026 fix]

Webhook signature mismatch caused by incorrect endpoint secret or missing raw body parsing—use raw request body and correct secret from Dashboard.

Stripe: Webhook Signature Verification Failing [2026 Fix]

TL;DR

Cause: You're passing the parsed JSON body instead of raw request bytes to signature verification, or using the wrong endpoint secret. Fix: Extract the raw request body *before* parsing JSON, and verify you're using the Endpoint Secret (not the API key) from Stripe Dashboard → Webhooks.

---

Exact Error Messages from Console Output

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

``` err: { type: 'StripeSignatureVerificationError', message: 'No signatures found matching the expected signature for payload.' } ```

``` Stripe.Signature.verifyHeader() returned false. timestamp=1704067200, signature_mismatch=true ```

``` Invalid sig_ header value. Expected format: t=<timestamp>,v1=<signature> ```

``` Webhook signature timestamp outside tolerance window. Tolerance: 300 seconds, skew: 1847 seconds ```

---

Broken Code vs. Fix

Problem 1: Parsing Body Before Verification

❌ BROKEN: ```javascript app.post('/webhook', express.json(), (req, res) => { // DON'T DO THIS - body is already parsed const sig = req.headers['stripe-signature']; const endpointSecret = 'whsec_...'; // req.body is a JavaScript object, not raw bytes const event = stripe.webhooks.constructEvent( req.body, // ← WRONG: already parsed sig, endpointSecret ); }); ```

✅ FIXED: ```javascript app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { // Correct: req.body is now a raw Buffer const sig = req.headers['stripe-signature']; const endpointSecret = 'whsec_test_...'; const event = stripe.webhooks.constructEvent( req.body, // ← CORRECT: raw bytes sig, endpointSecret ); res.json({received: true}); }); ```

Problem 2: Using Wrong Secret

❌ BROKEN: ```javascript // This is your API key, NOT the endpoint secret const endpointSecret = process.env.STRIPE_API_KEY; // Results in: "No signatures found matching..." ```

✅ FIXED: ```javascript // Go to Dashboard → Developers → Webhooks → Your endpoint → "Signing secret" const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; // whsec_... ```

Problem 3: Multiple Middleware Parsing the Body

❌ BROKEN: ```javascript // This parses body first, webhook handler gets parsed object app.use(express.json());

app.post('/webhook', (req, res) => { const event = stripe.webhooks.constructEvent( req.body, // Already parsed, signature fails req.headers['stripe-signature'], endpointSecret ); }); ```

✅ FIXED: ```javascript // Apply body parsing selectively app.use(express.json()); // For normal routes

// Webhook BEFORE general JSON parser app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { const event = stripe.webhooks.constructEvent( req.body, // Raw buffer req.headers['stripe-signature'], endpointSecret ); });

app.post('/api/other', (req, res) => { // This one gets parsed JSON as usual }); ```

---

Important Version Notes

  • stripe-node v8.0.0+: constructEvent() requires raw Buffer. Older versions may have different behavior—verify your package version with npm list stripe.
  • Express.js: The express.raw() middleware signature is consistent v4.16+, but if you're on v4.15 or older, consider upgrading or using bodyParser.raw().
  • Timestamp tolerance: By default, Stripe allows 5 minutes (300s) of skew. If your server clock is significantly out of sync, you may hit the timestamp error despite correct signature. [Check NTP sync on your server](https://docs.stripe.com/webhooks/best-practices#signature-verification).
  • ---

    Still Broken? Check These Too

    1. Endpoint Secret Mismatch Across Environments You copied the secret from *test mode* but your webhook is firing in *live mode* (or vice versa). Stripe generates separate secrets for each. Verify whsec_test_ vs. whsec_live_ prefixes match your environment.

    2. Cloudflare/Proxy Modifying Request Body Some reverse proxies or WAF rules may decompress or re-encode the webhook payload. Test by logging req.body.toString('hex') and comparing checksums with Stripe's Dashboard event logs.

    3. Localhost Testing Without Proper Tunnel Using curl to simulate webhooks bypasses Stripe's actual signing. Always use stripe listen --forward-to localhost:3000/webhook or a tool like ngrok/localtunnel with your real endpoint secret.

    ---

    Verification Checklist

  • [ ] Using express.raw({type: 'application/json'}) on webhook route only
  • [ ] endpointSecret starts with whsec_ (not sk_test_ or pk_test_)
  • [ ] Secret copied from Dashboard → Developers → Webhooks → Signing secret
  • [ ] No global express.json() middleware *before* the webhook route
  • [ ] Server clock is within 5 minutes of UTC
  • [ ] Testing with stripe listen locally, not manual curl requests
  • ---

    Quick Links

  • [Understanding webhook delivery](/?guide=stripe-webhook-best-practices)
  • [Testing webhooks locally](/?guide=stripe-local-testing)
  • [Official Stripe Webhook Verification Docs](https://docs.stripe.com/webhooks/signatures)
  • ---

    Found a different variation? Drop it in the comments—webhook issues often hide environment-specific quirks we can all learn from.

    🔥 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