logs.sologs.so
StatusPricingBlogDocs

Stripe webhooks

Webhook returned 200 but payment never processed

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.

Start freeSee pricing

Why HTTP 200 can still be a failed payment workflow

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.

  • The handler verified the signature, then returned 200 before writing the order.
  • The event type you handle is not the one Checkout actually sent.
  • A proxy, redirect, or page cache answered 200 without executing PHP or your Node route.
  • WooCommerce received the charge and never moved the order out of pending.

Stripe checks

  1. Open the event in the Stripe Dashboard

    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.

  2. See if the handler returns 200 before the work finishes

    If the route acknowledges first and fulfills later, a later exception never retries. Stripe already counted the delivery as successful.

  3. Confirm you handle the event you think you handle

    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.

  4. Match live versus test, and the current signing secret

    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.

  5. Verify the signature against the raw body

    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.

WooCommerce checks

  1. Check the WooCommerce order, not only the Stripe charge

    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.

  2. Open WooCommerce, then Status, then Scheduled Actions

    The Stripe gateway often queues payment_complete. Failed, pending, or stalled actions mean Stripe already 200ed and the order never moved to processing.

  3. Confirm WooCommerce is using the current webhook URL

    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.

  4. Bypass object cache and page cache for the webhook path

    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.

Cache and proxy checks

  1. Eliminate redirects in front of the endpoint

    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.

  2. Prove POST reaches origin with the Stripe-Signature header

    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.

  3. Reject HTML challenge pages as success

    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.

Monitor the expected business event

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.

  1. Watch the paid event, not endpoint availability

    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.

Start freeSee pricing

Related

  • Docs: send the fulfillment event
  • Dead cron check
  • When the feed looks fine and production died
  • LogSnag alternative
  • Healthchecks alternative
logs.sologs.so

Email when expected production activity goes quiet.

Product

  • Status demo
  • Pricing
  • Docs
  • Blog
  • Dead cron check
  • Stripe webhook silence
  • Healthchecks alternative
  • Instatus alternative
  • LogSnag alternative
  • Statuspage alternative
  • Contact

Builders

  • For Lovable
  • For Bolt
  • For Cursor
  • For v0
  • For Claude Code
  • For Codex
  • For 10x.app
  • For Xcode

Company

  • Support Teams
  • Privacy
  • Terms
© 2026 logs.soIncident email and public status pages.