The Silent Failure: Payments Land, Nothing Downstream Moves
Stripe webhook failures have a distinctive shape. The customer pays. The charge shows up green in the Stripe Dashboard. The money settles into your account on schedule. And that is where the good news stops.
The receipt email never sends. The order sits in "pending" while the customer waits. The subscription that should have activated their account never does. The invoice never appears in your accounting software. The booking never lands in your job system. Everything Stripe was supposed to trigger on your side of the fence simply does not happen.
What makes this failure mode dangerous is that the business keeps trading through it. Revenue still arrives, so nothing looks broken from the owner's chair. Webhook outages routinely run for weeks before anyone notices — and the person who notices is usually a customer chasing a receipt, not a monitoring system.
If that matches what you're seeing, the rest of this post is the diagnosis path we use on rescue jobs: where to read, what the response codes mean, the five causes that account for nearly every failing endpoint, and how to replay the backlog without emailing every customer twice.
Why Webhook Failures Are Silent by Design
A webhook is Stripe telling your server that something happened: a payment succeeded, a subscription renewed, a charge was disputed. Stripe sends an HTTPS POST to a URL you registered, and it expects a 2xx response back, quickly.
If your endpoint answers with anything else — an error code, a redirect, or silence — Stripe does not give up immediately. In live mode it retries the delivery with exponential backoff for up to three days. After that, the event drops out of the retry queue. If the endpoint keeps failing across days, Stripe emails a warning and can eventually disable the endpoint altogether, at which point deliveries stop entirely.
Here is the part that matters: none of this ever touches the payment. Stripe deliberately separates taking the money from notifying your systems, because a checkout should never fail just because your server is having a bad moment. It is the right design, and it has a consequence — the failure is completely invisible from the money path. Your customers see success. Your bank account sees success. Only your database knows something is wrong, and databases don't complain.
That is why diagnosing this is deliberate log-reading work. There is no alarm waiting to go off. You have to go and look.
Read the Logs Before Touching Code
Start in the Stripe Dashboard under Developers, then Webhooks. Select your endpoint and you'll see every recent delivery attempt: the event type, the timestamp, the HTTP response code your server returned, and — critically — the response body Stripe captured. That body often contains the exact error text your framework emitted. The Events page shows the raw event stream, whether or not delivery succeeded.
The response codes tell you where to dig:
| What Stripe shows | What it usually means | First place to look |
|---|---|---|
| 200, but nothing updated | Handler acknowledges then fails, or a dead consumer behind it | Application and worker logs |
| 400 | Signature verification rejecting the payload | Signing secret and raw request body |
| 401 / 403 | Auth middleware, firewall or WAF blocking Stripe | Middleware rules, IP allowlists |
| 404 | Endpoint URL is stale | Registered URL vs current routes |
| 3xx | A redirect — Stripe treats this as failure | HTTPS enforcement, www rewrites, domain moves |
| 5xx | Handler is crashing | Stack traces at the delivery timestamps |
| Timed out | Slow synchronous work before the response | Everything that runs before the 200 |
For local reproduction, the Stripe CLI earns its keep: stripe listen --forward-to localhost:3000/webhooks/stripe pipes test events to your machine, and stripe trigger payment_intent.succeeded fires synthetic ones on demand. You can watch the handler fail in real time instead of guessing from production logs.
The Five Usual Causes
Across the Stripe rescue work we do, five causes account for almost every failing endpoint.
1. Signature verification broke after a key rotation
Every webhook endpoint has its own signing secret — the whsec_... value. Stripe signs each delivery with it, and your handler verifies that signature before trusting the payload. Roll the secret in the Dashboard, or delete and recreate the endpoint, and a new secret is generated. If the environment variable on your server still holds the old one, every delivery from that moment fails verification with a 400.
Two quieter variants are worth checking. Verification runs over the exact raw bytes Stripe sent — if a global JSON body parser has already consumed and re-serialised the request, the computed signature will never match, even with the correct secret. And the signature includes a timestamp with a five-minute default tolerance, so a server with a drifting clock will reject perfectly valid deliveries.
2. Endpoint timeouts
Stripe expects your endpoint to respond promptly and treats a slow response the same as no response. The usual culprit is a handler doing real work before it answers: generating a PDF invoice, calling a third-party API, sending email — all synchronously, all before the 200 goes back.
The correct shape is to verify the signature, persist or enqueue the event, and return 200 immediately. The actual work happens afterwards, somewhere a retry can't stack up behind a slow render.
3. Stale or redirecting endpoint URLs
The endpoint URL registered in Stripe is configuration, and configuration outlives refactors. Domains migrate. Sites move from http to https. A www rewrite gets added at the CDN. A framework upgrade renames /api/webhooks/stripe to something tidier. Stripe does not follow redirects — a 301 or 302 counts as a failed delivery — and a 404 means the route simply is not there any more.
If the delivery log shows a wall of 3xx or 404 responses starting on a specific date, find out what shipped that day.
4. API version drift
Each webhook endpoint is pinned to a Stripe API version, and the event payload is rendered according to that version. Payload shapes genuinely change between versions: fields move, rename, or restructure. Upgrade your SDK or your account's default API version without checking the endpoint pin, and your handler starts reading fields that are no longer where it expects them.
This one produces the most dangerous variant of the failure: the handler doesn't crash, it returns 200, and quietly writes the wrong thing — or nothing — into your records. Green in Stripe, wrong in the database. It is exactly the case a verification layer is built to catch, because no delivery log will ever flag it.
5. Dead queue consumers
The sneakiest of the five. The endpoint does everything good practice dictates — verifies, enqueues, returns 200 — and Stripe's dashboard shows a perfect delivery record. But the worker that processes the queue died three weeks ago, or the queue filled, or the scheduled job that drains it was disabled during a deploy.
Every signal on Stripe's side is green because the HTTP contract is being honoured. The diagnosis has to cross the HTTP boundary: check queue depth, worker uptime, and the timestamp of the last successfully processed event. If the queue is growing while the processed timestamp goes stale, you've found it.
Replaying Events Safely
Once the root cause is fixed, there's a backlog: every event that fired during the outage. Stripe retains event data for around 30 days and lets you resend individual events from the Dashboard, or with the CLI: stripe events resend evt_....
Before replaying anything, four rules:
- Make the handler idempotent first. Every Stripe event carries a unique
evt_ID. Record processed IDs and skip duplicates before you replay a single event — otherwise the replay sends every "payment confirmed" email a second time. - Replay in chronological order. Subscription lifecycles and invoice states build on each other. Replaying
customer.subscription.updatedbefore thecreatedevent it follows produces records that never converge. - Replay only what the handler consumes. Filter to the event types your code actually processes. Blasting the full firehose back through proves nothing and muddies the logs.
- For gaps older than retention, reconcile from the API instead. Events are notifications; the underlying objects are the source of truth. List the PaymentIntents, invoices, or subscriptions for the gap window directly from the API and backfill from those. This is also the safer path when you're not certain what state the outage left behind.
Wire Alerting So the Next Failure Pages Someone
The fix isn't finished when the backlog clears. It's finished when this class of failure can't run silently again.
- Find out where Stripe's warning emails go. Stripe does email when an endpoint fails persistently — often to whichever address opened the account years ago, or a developer who has since moved on. Point notifications somewhere monitored.
- Alert on webhook error rates in your own logs. A spike of 400s or 500s on the webhook route should page someone the day it starts, not surface in a quarterly review.
- Alert on silence, not just errors. A zero-event window is a signal. If no
checkout.session.completedevent has arrived on a day the site took payments, something is wrong even though nothing errored. - Heartbeat the consumer. If a queue worker processes webhook jobs, it should report that it is alive and current. A dead consumer behind a green endpoint is exactly the failure a heartbeat exists to expose.
This is the standard we build to on our own products. Pink Slips NSW, our own platform, runs Stripe with full automation — hundreds of payments a year reconcile themselves, across sixteen integrations on one codebase — and the reconciliation checks exist precisely so a silent gap would surface as a discrepancy, not a customer complaint. The detail is in the Pink Slips NSW case study.
Fix It Yourself, or Call It a Rescue
Plenty of webhook failures are a one-hour fix for whoever has the logs open. If the break is recent, the response codes are clear, and someone on your side is comfortable reading server logs, work through the five causes above in order — most endpoints are back within an afternoon.
It becomes a rescue job when the shape is different: the endpoint has been failing for weeks and nobody can say when it started; records have drifted across Stripe, your database, and your accounting software until no two of them agree; the developer who built the integration has moved on and nobody knows where the handler code lives. That is the situation our integration rescue engagement is built for — read the delivery logs, establish the exact gap window, fix the root cause, replay and reconcile safely, then wire the alerting so the failure class is closed, not just the incident.
Know for Certain, Not Probably
If Stripe runs your payments and you cannot say with confidence that every event this quarter was delivered and processed, that is worth knowing before it costs you a customer. Our integration audit maps every webhook endpoint you have, reads the delivery history, verifies your downstream records against Stripe's, and hands you a prioritised fix list. It is a scoped engagement with a flat fee quoted up front, and we reply within 1 business day.
Share This Article
Spread the knowledge