logs.sologs.so
[Status][Pricing][Blog][Docs]

[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 200 is not enough]

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, Stripe stops. Stripe also stops if a redirect, edge or WAF response, custom cache rule, Worker, or another intermediary answered instead of the origin handler. Your Dashboard can still show 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 redirect, edge or WAF response, custom cache rule, Worker, or another intermediary answered instead of PHP or your Node route. Verify the body and an origin-side event, not only the status.
  • WooCommerce received the charge and never moved the order out of pending.

[Stripe]

Stripe checks

  1. [01]

    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. [02]

    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. [03]

    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. [04]

    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. [05]

    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]

WooCommerce checks

  1. [01]

    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. [02]

    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. [03]

    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. [04]

    Bypass object cache and page cache for the webhook path

    Exclude wc-api, wc-webhook, and the Stripe REST route from page cache. Redis or LiteSpeed object cache can leave order state stale after Stripe already saw 2xx. Cloudflare does not cache POST by default. A custom cache rule, Worker, or reverse proxy can still serve a cached GET of that path and skip PHP.

[Cache and proxy]

Cache and proxy checks

  1. [01]

    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. [02]

    Prove POST reaches origin with the Stripe-Signature header

    The Stripe POST, including the Stripe-Signature header, must reach origin. A WAF, Bot Fight, or Browser Integrity check does not necessarily return 200. Those features more often challenge or block. A custom cache rule, Worker, or another proxy can also rewrite the method or drop headers. Replay a Dashboard resend and log the raw method, headers, status, and body at origin.

  3. [03]

    Reject HTML challenge pages as success

    Do not treat HTTP status alone as proof the handler ran. An edge or WAF challenge, a maintenance page, a redirect, or a cached GET of /webhooks can keep origin code from running. Status is often a challenge, 403, or 3xx, not a successful 200. The body must be your handler output, and an origin-side event or log line should exist for that delivery.

[Monitor]

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. [01]

    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.

This page is free guidance. Silence monitoring and incident email are Pro-only at $19/mo. On Pro, logs.so watches that paid event and emails confirmed subscribers when the channel goes quiet. It detects a missing expected event. It cannot prove the root cause, and it cannot guarantee the payment workflow never fails. View a status page.

Start freeSee pricing

[Related]

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.