Stripe webhooks
A 200 from your webhook URL means Stripe got an HTTP success. It does not mean the order was marked paid, the entitlement was granted, or WooCommerce left pending. Those are different systems that can fail after the response is already sent.
Stripe retries only on network failure or a non-2xx status. If your route returns 200 and then throws, or returns 200 from a cache that never ran the handler, Stripe stops. Your Dashboard still shows a successful delivery. The customer still paid. Your app still has nothing to fulfill.
Developers, then Events. Confirm checkout.session.completed or payment_intent.succeeded exists, then open Webhooks and inspect the delivery. A 200 there is only the HTTP response.
If the route acknowledges first and fulfills later, a later exception never retries. Stripe already counted the delivery as successful.
charge.succeeded is not the same as checkout.session.completed. Delayed methods (bank debit, BNPL) can complete the session before funds clear. Listen for checkout.session.async_payment_succeeded or payment_intent.succeeded when that is the paid moment.
A test endpoint on a live secret, or a rotated endpoint secret left in an old env, can accept traffic that never reaches the production handler you are staring at.
If the framework already parsed JSON, constructEvent fails. Swallowing that error and still returning 200 looks identical to a healthy delivery. Return 400 on a bad signature so Stripe retries.
The charge can succeed while the order stays pending or on-hold if the Stripe plugin, Action Scheduler, or a custom status mapping never ran.
The Stripe gateway often queues payment_complete. Failed, pending, or stalled actions mean Stripe already 200ed and the order never moved to processing.
WooCommerce Stripe settings list the endpoint Stripe should call (often ?wc-api=wc_stripe or the wc-stripe REST route). An old permalink, a staging clone, or a second store URL will 200 on the wrong site.
Redis, LiteSpeed, or a full-page cache that stores POST /?wc-api= can return a stale 200 without running PHP. Exclude wc-api, wc-webhook, and the Stripe REST route.
Cloudflare HTTPS rewrites, www versus apex, trailing slashes, and middleware that 301s to a localized path all look healthy in a browser and fail for Stripe.
A WAF, Bot Fight, or cache rule that strips signatures or turns POST into GET will still answer 200 on a probe. Replay a Dashboard resend and log the raw method and headers.
An edge challenge, maintenance HTML, or a cached GET of /webhooks can still be status 200. The body must be your handler output, not a CDN interstitial.
A probe against the webhook URL only tells you the path is reachable. The failure people search for is silence after a successful charge: no checkout.session.completed handled, no order paid, no license issued. Instrument that fulfillment event on a channel. Set the rhythm you already know (a checkout every few minutes on a busy store, or nightly on a quiet one). Page when that event stops, even if uptime stays green.
Uptime on /webhooks only proves a TCP hop. Log fulfillment (order paid, license issued, Woo status processing) on a channel and page when that rhythm stops.
logs.so can watch that paid event and email confirmed subscribers when the channel goes quiet. View a status page.