Stripe webhooks failing silently — and how to catch it

A customer pays. Stripe takes the money. Your app never hears about it, so their account stays on the free plan — and the first you learn of it is a support email that starts with "I was charged but…".
Webhooks are how Stripe tells your app that money moved. When they fail, they fail in the worst possible way: the payment still succeeds, so nothing looks broken until a user tells you it is. Here's how they break, what Stripe does about it, and how to see it coming.
How webhooks fail without a sound
Stripe considers a webhook delivered only if your endpoint returns a 2xx status. Anything else — a 400, a 500, a timeout, a deploy that briefly 404s the route — counts as a failure and gets retried. The catch is that your users never see any of this. They paid; Stripe's side is fine.
The usual culprits:
- The signature check rejects the event. Verifying the
Stripe-Signatureheader needs the raw request body. Most frameworks parse the body to JSON before you see it, which changes the bytes and makes every verification fail with a 400. - The handler throws. A bug in the code that provisions access errors out and returns 500 — the payment went through, but the thing the payment was supposed to unlock never happened.
- It's too slow. Do heavy work inline and the response times out; Stripe records a failure even though your code eventually finished.
- The endpoint moved. A route rename or a bad deploy points Stripe at a URL that no longer answers.
What Stripe does when your endpoint fails
Stripe doesn't give up immediately — but it doesn't wait forever either. In live mode it retries a failing endpoint with exponential backoff for up to three days.
If it's still failing after that, Stripe automatically disables the endpoint and emails the account. Once disabled, new events aren't delivered there at all until you re-enable it by hand from the Dashboard.
- 2xx
- the only response Stripe counts as delivered
- 3 days
- it retries a failing endpoint, then stops
- auto-disabled
- endpoint switched off + emailed after that
- raw body
- required to verify the signature — parsers break it
Recovering the events you missed
Not all is lost after a failure. Stripe keeps the events, and you can replay them:
- In the Dashboard, open Developers → Webhooks, pick your endpoint, and look at its recent deliveries. Failed attempts are listed with the response code they got.
- Fix the handler and resend the failed events from that same view, or with the Stripe CLI's
stripe events resend <id>. - If the endpoint was auto-disabled, re-enable it first — otherwise the resent events have nowhere to go.
This only works if you catch it inside the window and your handler is idempotent, so replaying an event you partly processed doesn't double-charge or double-provision.
Build the handler so it can't fail quietly
Most silent webhook failures trace back to two things: the raw-body signature trap, and returning the wrong status code. This prompt builds a handler that verifies properly, stays idempotent, and only reports success when the work actually succeeded:
Write a production-ready Stripe webhook handler for my app. Requirements: - Read the RAW request body (not JSON-parsed) and verify the Stripe-Signature header against my webhook signing secret using stripe.webhooks.constructEvent. If verification fails, return 400 and do nothing else. - Make it idempotent: Stripe can deliver the same event more than once, so record processed event IDs and skip an event I've already handled. - Do the real work (e.g. provisioning access on checkout.session.completed) inside try/catch. On success return 2xx. On a real processing error return 500 so Stripe retries — but never return 2xx just to stop the retries when the work didn't actually succeed. - Keep the handler fast: acknowledge quickly and, if the work is slow, hand it to a background job rather than blocking the response. - Tell me the framework-specific gotcha for reading the raw body (I'll tell you my framework), and which events to listen for to activate a paid subscription. Show me the full handler and the list of events to subscribe to.
Stripe's own webhooks documentation has the full event reference and signing details.
See the failure the day it starts
The Dashboard shows failed deliveries — but only if you go and look, and nobody checks the webhooks page on a quiet Saturday. That's the same blind spot a silently failing cron has: the information exists, but nothing pushes it to you.
Tell Me When Down watches the endpoint your billing depends on, so the moment it starts returning errors instead of 2xx, you get an email — inside the three-day window, while replay is still an option, and long before a customer writes in.
Don't let a broken webhook cost you a customer.
Join Tell Me When Down free and we'll watch your Stripe webhook endpoint around the clock. The moment it starts failing, you hear about it — in time to fix it and replay the events, not after Stripe has disabled it.
spot something wrong or out of date? [email protected] — we'll fix it