Stripe: webhook signature verification failing [2026 fix]

Webhook signature mismatch caused by endpoint secret misconfiguration or timestamp skew; verify your signing secret and clock sync.

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

TL;DR

Cause: Your endpoint is using the wrong webhook signing secret or your server's clock is skewed by >5 minutes. Fix: Verify STRIPE_WEBHOOK_SECRET env var matches your dashboard webhook endpoint, and sync your server time with NTP.

---

Real Console Error Messages

Here are the exact errors you'll see:

``` Error: No signatures found matching the expected signature for payload. Attempted to construct signature using the following secret: 'whsec_live_***' but no signatures found in the 'stripe-signature' header. ```

``` StripeSignatureVerificationError: No valid signature found. ```

``` Webhook error: Unable to extract timestamp and signatures from header. Header value: undefined ```

``` Error: Timestamp outside the tolerance window: received 1704067200, current 1704067920 (timestamp difference: 720 seconds exceeds max tolerance of 300 seconds) ```

``` Signature verification failed. Expected sig: sha256=abc123..., got: sha256=xyz789... ```

---

The Problem: Side-by-Side Code Fix

❌ BROKEN CODE

```javascript // webhook.js - WRONG const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const express = require('express'); const app = express();

app.post('/webhook', express.json(), (req, res) => { // WRONG: Using parsed JSON body directly const event = req.body; // WRONG: Not verifying signature at all if (event.type === 'payment_intent.succeeded') { console.log('Payment successful:', event.data.object.id); res.json({received: true}); } }); ```

✅ CORRECT CODE

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

// CRITICAL: Must be raw body, NOT parsed JSON app.post('/webhook', express.raw({type: 'application/json'}), // Raw buffer required (req, res) => { const sig = req.headers['stripe-signature']; const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; // whsec_live_*** let event; try { // Signature verification happens HERE event = stripe.webhooks.constructEvent( req.body, // Raw body buffer sig, // Signature from header webhookSecret // Your endpoint's signing secret ); } catch (err) { console.error('Webhook signature verification failed:', err.message); return res.status(400).send(Webhook Error: ${err.message}); } // Only process AFTER verification passes if (event.type === 'payment_intent.succeeded') { console.log('Payment successful:', event.data.object.id); } res.json({received: true}); } ); ```

Key differences:

  • express.raw() instead of express.json() — the signature is computed on the raw bytes
  • stripe.webhooks.constructEvent() verifies the signature before parsing
  • Explicit error handling for verification failures
  • ---

    Verification Checklist (Do This Now)

    1. Verify Your Webhook Secret

    Go to [Stripe Dashboard → Developers → Webhooks](https://dashboard.stripe.com/webhooks). Find your endpoint:

    ```bash

    Should match exactly

    echo $STRIPE_WEBHOOK_SECRET

    Should output: whsec_live_4eC39HqLyjWDarhtT1g3bZ8y or whsec_test_...

    ```

    If it says pk_live_... or sk_live_..., you copied the wrong key. Copy the Signing secret under "Endpoint details."

    2. Check Server Clock Skew

    Stripe rejects signatures older than 5 minutes. Run:

    ```bash

    Linux/Mac

    date && curl -s https://worldtimeapi.org/api/timezone/Etc/UTC | grep unixtime

    If difference > 300 seconds, sync your clock

    sudo ntpdate -s time.nist.gov # or use timedatectl on systemd ```

    3. Validate Raw Body Handling

    Add this debug endpoint temporarily:

    ```javascript app.post('/webhook-debug', 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 header:', req.headers['stripe-signature']); res.json({ok: true}); }); ```

    Test with Stripe CLI:

    ```bash stripe listen --forward-to localhost:3000/webhook-debug stripe trigger payment_intent.succeeded ```

    ---

    Still Broken? Check These Too

    1. Multiple middleware parsing JSON: If you have app.use(express.json()) globally, it parses the body before your raw middleware sees it. Move webhook routes BEFORE global JSON middleware, or skip JSON parsing for /webhook* routes.

    2. Ngrok/proxy stripping the signature header: Some reverse proxies drop custom headers. Verify headers are passing through: curl -v https://your-tunnel.ngrok.io/webhook and check response includes stripe-signature.

    3. Environment variable not loaded: In Node.js, process.env is read at startup. If you added STRIPE_WEBHOOK_SECRET to .env after starting the server, restart: node app.js (not just saving the file).

    ---

    Related Guides

  • [Stripe API Key Rotation: Avoid the downtime trap](/?guide=stripe-key-rotation)
  • [Express Middleware Order: Why Your Auth Middleware Isn't Running](/?guide=express-middleware-order)
  • ---

    Official Documentation

    [Stripe Webhook Signature Verification](https://stripe.com/docs/webhooks/signatures) [Stripe Node.js Library Reference](https://stripe.com/docs/api/libraries)

    ---

    Found a different variation? Drop it in the comments below.

    🔥 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