Stripe: webhook signature verification failing [2026 fix]

Webhook signature mismatch: endpoint secret misconfiguration or timestamp validation failure. Verify correct secret in environment variables and check system clock skew.

Stripe: Webhook Signature Verification Failing [2026 Fix]

TL;DR

Cause: You're using the wrong endpoint secret (publishable key instead of signing secret) OR your server's system clock is >5 minutes out of sync. Fix: Replace STRIPE_WEBHOOK_SECRET with the signing secret from Stripe Dashboard > Developers > Webhooks, and ensure NTP is synced on your server.

---

Exact Error Messages

You'll likely see one of these in your production logs at 2am:

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

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

``` Error: Timestamp outside the tolerance window ```

``` SignatureVerificationError: No valid signature headers detected ```

``` Error validating request signature: Invalid signature ```

---

The Problem: Side-by-Side Code

❌ BROKEN CODE

```javascript // Express webhook endpoint - INCORRECT const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { // WRONG: Using publishable key or incomplete secret const sig = req.headers['stripe-signature']; try { const event = stripe.webhooks.constructEvent( req.body, sig, process.env.STRIPE_API_KEY // ❌ WRONG: This is the API key, not webhook secret ); res.json({received: true}); } catch (err) { return res.status(400).send(Webhook Error: ${err.message}); } }); ```

✅ FIXED CODE

```javascript // Express webhook endpoint - CORRECT const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { const sig = req.headers['stripe-signature']; try { const event = stripe.webhooks.constructEvent( req.body, sig, process.env.STRIPE_WEBHOOK_SIGNING_SECRET // ✅ CORRECT: Use signing secret ); // Handle specific events switch(event.type) { case 'payment_intent.succeeded': console.log('Payment succeeded'); break; case 'charge.failed': console.log('Charge failed'); break; } res.json({received: true}); } catch (err) { console.error('Webhook signature verification failed:', err.message); return res.status(400).send(Webhook Error: ${err.message}); } }); ```

Key Differences

1. Secret source: Use STRIPE_WEBHOOK_SIGNING_SECRET (starts with whsec_), NOT STRIPE_SECRET_KEY (starts with sk_) 2. Environment variable naming: Be explicit and consistent 3. Error logging: Always log the raw error for debugging

---

How to Get the Right Secret

1. Go to [Stripe Dashboard](https://dashboard.stripe.com/) 2. Navigate to Developers → Webhooks 3. Click on your endpoint URL 4. Scroll to Signing secret section 5. Click Reveal to show the secret (format: whsec_live_xxxxx) 6. Copy and paste into your .env file:

``` STRIPE_WEBHOOK_SIGNING_SECRET=whsec_live_xxxxx ```

Critical: This is different for:

  • Test mode vs. Live mode (get both)
  • Each webhook endpoint URL (if you have multiple)
  • ---

    Clock Skew Issue

    Stripe rejects signatures with timestamps older than 5 minutes. If your server clock is incorrect:

    ```bash

    On Linux/Mac - check system time

    date

    Sync with NTP

    sudo ntpdate -s time.nist.gov

    Or use timedatectl

    sudo timedatectl set-ntp true ```

    If you're on AWS/Docker/Kubernetes, ensure:

  • EC2 instance has amazon-cloudwatch-agent configured
  • Docker container has correct TZ environment variable
  • Pod has proper volume mount for /etc/localtime
  • ---

    Still Broken? Check These Too

    1. [Raw body requirement](/guide=stripe-raw-body): Stripe signature verification requires the raw HTTP body, not parsed JSON. Ensure express.raw() middleware is applied *before* JSON parsing for this route only.

    2. Endpoint URL mismatch: The webhook endpoint URL registered in Stripe Dashboard must exactly match your actual endpoint. Verify in Dashboard → Developers → Webhooks that the URL matches your server's path (including protocol: https://, not http://).

    3. [Timeout configuration](/guide=stripe-webhook-timeout): Stripe expects a response within 30 seconds and retries failed webhooks. Long database operations inside the webhook handler cause timeouts—move heavy work to background jobs using Redis/Bull or AWS Lambda.

    ---

    Version-Specific Notes

    I'm uncertain about exact behavior differences in stripe-node versions before 8.0.0. If you're using an older version (check package.json), you may need to pass additional options to constructEvent(). Consult the [official Stripe Node.js library changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md).

    ---

    Official Resources

  • [Stripe Webhooks Documentation](https://stripe.com/docs/webhooks/quickstart)
  • [Stripe Signature Verification Guide](https://stripe.com/docs/webhooks/signatures)
  • [Node.js Stripe Library Reference](https://github.com/stripe/stripe-node)
  • ---

    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