Stripe Webhook Signature Verification Errors: Causes and Fixes

SendPromptly Team
4 min read

Your Stripe dashboard shows the same webhook event retried five, six, ten times, and every attempt ends the same way: signature verification failed, 400 returned. The checkout, invoice, or subscription update behind that event is stuck. Stripe keeps retrying because your handler keeps rejecting it, and nothing downstream happens until that stops.

This failure is loud in your logs but invisible everywhere else. The customer often completed checkout on Stripe’s side without a problem — they just never got the access, receipt, or plan update your app was supposed to send back. The first sign is usually a spike of failed attempts in the Stripe dashboard, not a support ticket, which means it is worth checking before someone complains.

The good news is that it is usually not mysterious. It is almost always one of four things: the body changed before verification, the endpoint secret is wrong, the event came from the wrong mode, or timestamp tolerance handling is broken.

Use the raw request body

Stripe signature verification must use the exact raw request body Stripe sent. If your framework parses JSON first, changes whitespace, changes encoding, or reserializes the body, the computed signature will not match.

In Express, use a raw body parser for the webhook route:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import express from "express";
import Stripe from "stripe";

const app = express();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

app.post("/stripe/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["stripe-signature"];

  let event;
  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (error) {
    console.error(`Stripe signature failed: ${error.message}`);
    return res.sendStatus(400);
  }

  console.log(`verified ${event.id} ${event.type}`);
  return res.sendStatus(200);
});

app.listen(3000);

Do not put express.json() in front of this route unless you explicitly exclude the webhook path.

Confirm the endpoint secret

Stripe signing secrets are endpoint-specific. A secret from one endpoint will not verify events sent to another endpoint. Test mode and live mode also use different webhook endpoints and secrets.

Check:

  • The endpoint URL in Stripe Dashboard
  • The signing secret for that exact endpoint
  • Whether your app is using live or test environment variables
  • Whether a recent secret rotation changed Stripe but not your deployment config

If local verification works with stripe listen but production fails, you may be using the CLI forwarding secret locally and a different dashboard endpoint secret in production. That is expected; both must be configured separately.

Watch middleware and proxies

Common framework-level causes:

  • JSON body parsing before verification
  • CSRF middleware rejecting the request before the webhook route
  • Auth middleware trying to require a user session
  • Serverless platform body transformation
  • Reverse proxy decompression or request-size limits

The webhook route should be narrow and explicit: accept the raw body, verify the signature, durably accept the event, then return a 2xx quickly.

Check timestamp tolerance

Stripe signatures include a timestamp. Most official library examples reject signatures outside a tolerance window. That helps reduce replay risk, but it also means server clock drift can break verification.

Check your production server time:

1
date -u

If the server clock is wrong, fix NTP or platform time sync. Do not solve clock drift by disabling timestamp checks globally.

Separate verification from fulfillment

Signature verification only proves the request is authentic. It does not prove your app applied the business effect. After verification, your handler still needs logs, queue monitoring, idempotency, and outcome checks.

If Stripe shows delivery failures with 400 responses, fix signature verification first. If Stripe shows 2xx but your app did not update, use the webhook delivered but nothing happened guide.


Fixing signature verification only gets the event past the front door — it does not prove your app finished the job afterward. SendPromptly picks up right where this guide ends: once your handler verifies and accepts an event, it monitors whether the expected business outcome (access granted, credits added, subscription updated) actually completed, and opens an incident when it does not. See how it works →