Start with Stripe's delivery log
Open Workbench → Webhooks, pick your endpoint, and open the Event deliveries tab. Each event shows as Delivered, Pending or Failed, with the HTTP status code of each try. (Stripe: Debug webhook integrations)
- The event isn't listed at all. The cause is in your Stripe setup: causes 1–3 and 6.
- The event is listed with an error. The cause is your endpoint or your network: causes 4, 5 and 7.
1.The endpoint is turned off (or was deleted)
Every webhook endpoint has a status of enabled or disabled. (API: Webhook Endpoint object) Stripe won't resend events made while an endpoint was turned off. (Manage event destinations) If the endpoint is off or deleted when a retry is due, Stripe stops retrying that event. (Automatic retries)
How to check
- Open the endpoint in Workbench → Webhooks and make sure it's enabled.
- Or, with the API, get the endpoint and read its
status.
2.The event type isn't selected
An endpoint only gets the event types you pick for it. Stripe suggests picking only the ones you need. (Best practices) So if your code waits for checkout.session.completed but the endpoint only gets payment_intent.succeeded, your code never runs.
How to check
- Compare the endpoint's event list with the
event.typevalues your code handles. - Edit the endpoint in Workbench, or update it with the API.
3.Sandbox (test) and live mode don't match
Sandbox and live are separate, each with its own keys. Sandbox keys start with sk_test_, rk_test_ or pk_test_. Live keys start with sk_live_, rk_live_ or pk_live_. (API keys)
Webhook signing secrets are not API keys. If one endpoint serves both modes, its secret is different in each. (Endpoint secrets) For Connect, sandbox accounts need their own endpoints. (Connect webhooks) A common case: it all worked in testing, but nobody made a live endpoint.
stripe listen uses a sandbox by default. Live events need the --live flag. (CLI: listen)
How to check
- Switch the Dashboard to live mode and make sure a live endpoint exists for your real URL.
- Make sure your live app uses the live
whsec_secret.
4.Signature checks fail, so your code rejects the event
Stripe did send the event, but your code answered 400. To you, that looks like "not firing." The error No signatures found matching the expected signature for payload means one of three inputs is wrong: the raw body, the Stripe-Signature header, or the endpoint secret. (Signature errors)
- Wrong secret. The
stripe listensecret is not the Dashboard endpoint's secret. - The body changed first. Stripe needs the exact raw body. Tools that parse JSON first break the check. With Express, put the webhook route before
app.use(express.json()). - Clock drift. Stripe's libraries allow 5 minutes by default, so keep your server clock synced with NTP. (Preventing replay attacks)
How to check
- Compare the secret your code really loads with the one in Workbench (log only its last few characters).
- Log the
Stripe-Signatureheader. It should look liket=…,v1=…. - Make sure you pass the unparsed body to the check.
5.Your endpoint returns non-2xx, redirects or times out
Stripe counts any non-2xx answer as a failed delivery, redirects included. (Status codes)
- 3xx: a failure. Register the final URL the redirect leads to.
- 4xx: blocked access (401, 403, 405) or the wrong path (404).
- 5xx: your app hit an error. Check your logs.
- TLS errors: Stripe needs TLS 1.2 or higher.
- Timeouts: answer 2xx fast and do slow work later.
Retries: in live mode Stripe retries for up to three days. In a sandbox, three times over a few hours. You can resend by hand from the Dashboard for up to 15 days, or with stripe events resend for up to 30 days. (Manual retries) For subscriptions, Stripe also emails you when a webhook is failing. (Webhooks with subscriptions)
How to check
- Open a failed delivery in Event deliveries and read the status code.
- To find what you missed, call
GET /v1/eventswithdelivery_success=false. It covers the last 30 days. (Process undelivered events)
6.Connect: you listen to the wrong account
On a Connect platform, each destination listens to one of two scopes: your account (your platform's events) or connected accounts (events on their resources, like direct charges). If you need both, make both. (Event destinations for Connect) Events from connected accounts have a top-level account field. (Connect webhooks)
How to check
- Check which scope your destination uses.
- Locally, test with
stripe listen --forward-connect-to localhost:PORT/webhookfor connected accounts, and--forward-tofor your own.
7.A firewall, login wall or CSRF check blocks Stripe
Your endpoint must be a public HTTPS URL that takes POST. (Set up your endpoint) Common blockers:
- A login in front of the route. Login walls, preview-deploy protection or JWT checks answer 401 or 403.
- CSRF checks. Rails, Django and others may reject the POST. Stripe says to exempt the webhook route. (CSRF)
- An old IP allowlist. Stripe lists its webhook IPs at stripe.com/files/ips/ips_webhooks.txt. (Domains and IP addresses)
How to check
- Send a plain
POSTto your webhook URL from outside your network. You should get your code's answer, not a login page or a redirect. - If you filter by IP, compare your list with Stripe's current list.
Also check: API version
An event's shape depends on the API version set when it happened. Changing the version later doesn't change old events. (API versioning) So delivery can work while your code can't read the event.
A 5-minute test with the Stripe CLI
stripe login stripe listen --forward-to localhost:4242/webhook # prints a whsec_ secret for local use # in another terminal: stripe trigger payment_intent.succeeded
If the event reaches your code this way but never in production, your code is fine. The problem is the live endpoint: causes 1–3, 5 and 7. stripe listen --load-from-webhooks-api listens with the endpoints you already set up. (Test locally)
Where CheckoutAlarm fits
CheckoutAlarm is new. Once you prove you own your site, it:
- loads your page on a schedule and checks the status, speed, and a title or phrase you pick
- uses a read-only Stripe key to email you about events Stripe couldn't deliver, endpoints that are turned off, and sales that stop
- emails you after two page misses in a row, or the first Stripe problem, and again when it's fixed
It doesn't fix webhooks for you. For the details of each delivery, Stripe's Event deliveries tab is still the place to look.
Questions
- Why does my Stripe webhook work in test mode but not live?
- Sandbox and live use separate keys, and an endpoint's signing secret is different in each mode. Make sure a live-mode endpoint exists for your production URL and that production uses the live whsec_ secret.
- How long does Stripe retry a failed webhook?
- In live mode, for up to three days with exponential backoff. In a sandbox, three times over a few hours. You can resend by hand from the Dashboard for up to 15 days, or with the Stripe CLI for up to 30 days.
- Does Stripe treat a redirect as a successful webhook delivery?
- No. Stripe treats 3xx responses as failures. Register the final URL the redirect leads to.
- Why do I get "No signatures found matching the expected signature for payload"?
- One of three inputs is wrong: the endpoint secret (CLI and Dashboard secrets differ), the raw request body (it must not be parsed or changed), or the Stripe-Signature header.
- How do I test a Stripe webhook locally?
- Run stripe listen --forward-to localhost:4242/webhook. Then, in another terminal, run stripe trigger payment_intent.succeeded.
Stripe docs checked on Oct 17, 2026.