Skip to main content

Blog · Spoke

Stripe Webhook Not Working? Fix Missing Payments

Stripe says delivered but your handler never fires? Five checks for missing payment notifications: endpoint URL, signing secret, test vs live mode.

By Dima K. Published

Stripe Webhook Not Working — How to Fix Missing Payment Notifications

You are looking at a row of green checkmarks in Stripe that say every webhook delivered, and a server log that shows none of them ever arrived. Both screens are telling the truth. That is the trap you are in. Stripe genuinely sent the events. Your handler genuinely never ran. And somewhere between those two facts your customers are getting charged while their accounts stay locked, silently, with nothing on fire and no error to grep for.

“You built the webhook endpoint. Stripe says it delivered. Your logs show nothing. The order never fulfilled.”

DEV Community / deadletter, 2024

You found out the worst way, the way nearly everyone does: a user emailed asking why their account is locked after they paid. So before you touch a single line of code, here is what to check.

The fastest fix: verify the endpoint URL first

Check that Stripe is pointing at the right address. This one fix clears roughly 40% of silent webhook failures with no code change at all.

Open Stripe, Developers, Webhooks, your endpoint, and compare the configured URL against what your server is actually listening on after your last deploy. Bolt.new, Vercel, and Netlify can all quietly reshape your URL structure on a redeploy, and Stripe has no way to know it moved. If the two do not match exactly, every event is being delivered to an address where nothing lives. Click “Resend” on the most recent event and watch your server logs in real time. Nothing arrives? Mismatch confirmed. Update the endpoint in Stripe, resend, and you are back.

Five checks when the URL is correct

“Your endpoint returns 200 OK to Stripe. But your handler never fires.”

DEV Community / James Brown, 2024

Delivered, but your handler never fired

This is the signature failure of the whole category. Stripe marks the event delivered, your order or email or subscription logic never processes, and no error appears anywhere. Add a single log line at the very top of your webhook handler, before any parsing, before the signature check, before anything. Then resend from Stripe’s dashboard and watch for it. If that line never prints, something in front of your code, a catch-all route or a middleware layer or a reverse proxy, returned 200 to Stripe before the request ever reached your handler. Stripe saw the 200, called it delivered, and moved on. Your code was never in the room. Check your routing configuration first, because no amount of staring at business logic will help when the request never got that far.

The signature check is rejecting Stripe silently

Different shape: requests do arrive, then stop dead in your logs, and Stripe marks them failed after getting back an error response. That is signature verification refusing the request before your logic ever runs. The culprit is almost always the signing secret. STRIPE_WEBHOOK_SECRET in your production environment has to match Stripe’s dashboard value exactly, down to trailing whitespace, and a single stray character invalidates every event. Pull it from Stripe under Developers, Webhooks, your endpoint, Signing secret, click reveal, and compare. The alert you want here is “Stripe webhook rejected, signing secret mismatch, check STRIPE_WEBHOOK_SECRET in production env vars.” The one you get is an unreadable verification error that buries the one fact you need.

Test mode is armed, live mode is empty

Webhooks fired perfectly all through development, then live production payments produce nothing at all. That clean split is the tell. Stripe’s test-mode and live-mode endpoints are entirely separate worlds, and a test-mode endpoint never sees a real transaction. Open Developers, Webhooks, and check which mode the endpoint lives in. Your production app needs one configured in live mode. This catches a lot of fast-shipped apps, because everything gets tested thoroughly in test mode and the live-mode endpoint is a separate setup step that is genuinely easy to forget on launch day.

The deploy moved the URL out from under Stripe

Webhooks worked right up until the last deploy and stopped the moment after. Pull your hosting provider’s deployment logs and look for any shift in the URL or path. AI-assisted deploys are especially good at moving a path without making it obvious you moved it, and some providers hand preview deployments their own URLs that only work in test mode. It’s the same preview-versus-production gap that breaks vibe-coded apps after a deploy, walked through in bolt.new preview vs production: the environment that worked is not the environment that’s live. Confirm Stripe is pointing at your stable production URL, not a preview that happened to work the day you set it up, then update the endpoint if it drifted.

Your server is returning 500, and Stripe is about to give up

The cruelest one, because it is on a timer.

“For recurring products, this is where 20-40% of involuntary churn comes from.”

heze (commenter), IndieHackers, 2025

Here Stripe shows the events as “failed” with several retry attempts, and subscriptions start canceling on their own. Click into a failed event in the webhook log to see every retry and the exact response code behind it. A 500 means your handler threw an unhandled exception. Stripe will retry that event over about three days, roughly five attempts with growing gaps between them, and then it stops trying forever. Anything your subscription logic should have done in that three-day window has to be replayed by hand from the dashboard afterward, and every one you miss is a paying customer who quietly churns out without ever telling you why.

The silent revenue gap you cannot watch from inside

Run the numbers on the situation you are in. Some number of events, delivered to a void, over some number of days, and the first sensor that fired was a customer’s frustration. Stripe’s “delivered” and your handler actually running are two different facts, and almost no Stripe setup has any way to see the space between them from the outside. From inside your code, a webhook that never arrived looks exactly like a quiet day. There is nothing to log, because nothing happened, and “nothing happened” is precisely what a missed payment looks like too.

From inside, a missed payment looks exactly like a quiet day, which is the same trap behind every silent automation failure: the absence of an error is not the presence of success. It’s also why a crashed AI-built app can keep returning healthy responses while doing none of the work it’s supposed to.

That blind spot is the thing to close. NoCrash watches whether your live app is actually processing what it should, payments flowing, accounts unlocking, from the outside where your customers stand, and tells you in plain language the moment that stops. Connect your app free at nocrash.io and the next void swallows a message to you instead of three days of revenue.

So add that one log line at the top of your handler today, and set up the outside check while you are in there. Future-you, reading “payments stopped processing 11 minutes ago” instead of a refund request from a locked-out customer, will be glad you did.

— NoCrash

Common questions

Frequently asked

Stripe shows the webhook delivered, why didn't my handler run?
"Delivered" means Stripe got a 200, not that your handler ran. A reverse proxy may have returned 200 before your code saw the request. Add a log line at the top of your handler, resend the event from Stripe's dashboard, and check whether it appears.
How do I tell if it's signature verification or my code?
Add a log line before the signature check. If it appears but the handler stops, compare `STRIPE_WEBHOOK_SECRET` in production to the dashboard value. If the line never appears, your server isn't receiving the request.
Stripe says "webhook failed", does that mean the customer got charged or not?
The charge and the webhook are independent. The payment went through; your fulfillment or subscription logic didn't run. Check Stripe's event log for what was charged and replay the relevant events after fixing the handler.
How long does Stripe retry before giving up?
Over approximately 3 days with exponential backoff, roughly 5 attempts. After that, retries stop. Subscription events not processed in those 3 days need manual replay from the Stripe dashboard.
How do I prevent this from happening next time?
Three checks after every deploy: confirm the endpoint URL in Stripe matches your production URL, verify `STRIPE_WEBHOOK_SECRET` is correct, and resend a test event. For ongoing visibility into whether payments are flowing normally, that's the gap NoCrash is built to close.

Stop finding out from your customers.

One morning message telling you what ran clean and what didn’t. Free forever on 3 things to watch.