--- title: WooCommerce orders still pending after a successful Stripe payment description: Stripe charged the customer but WooCommerce never moved the order out of Pending. Why webhook failures leave orders stranded, and a plugin that finds them. source: https://steem.dev/blog/woocommerce-stripe-pending-order-reconciliation published: 2026-11-11 updated: 2026-11-11 site: Steem — AI WordPress plugin generator --- # WooCommerce orders still pending after a successful Stripe payment Stripe shows a succeeded charge. The customer's card was billed. The WooCommerce order is still in Pending Payment, the confirmation email was never sent, and the customer's account reads 'awaiting payment.' This surfaces when the customer contacts support, or when you notice an order that never moved during a morning audit. The gap comes from how WooCommerce processes Stripe payments: asynchronously, through webhooks. Stripe sends a signal when payment succeeds; WooCommerce receives it, verifies it, and transitions the order. When that signal fails to land, the charge is real but the order stays frozen. A periodic check against the Stripe API finds those orders and completes them before the customer has to ask. ## What one build covers - Scheduled check of Pending Payment orders against the Stripe payment intent API - Automatic transition to Processing when Stripe confirms payment succeeded - Order note recording the payment intent ID and that reconciliation resolved it - Settings field for the Stripe secret key under WooCommerce settings ## What it does not cover - Other payment gateways including PayPal, Square or Braintree - Split capture flows where authorization and capture happen in separate steps - Refund, dispute or chargeback events from Stripe - Diagnosing why webhook deliveries are failing or fixing the endpoint URL - Orders where the customer's payment genuinely failed on Stripe's side ## The prompt ```text Build a WordPress plugin called Stripe Order Reconciler. Add a settings field for a Stripe Secret Key under WooCommerce > Settings > Advanced, stored in wp_options. Register a WP-Cron event using wp_schedule_event that runs every fifteen minutes. In the callback, use wc_get_orders() to find orders with status pending-payment that were created more than ten minutes ago and have order meta _stripe_intent_id set to a non-empty value. For each matching order, make a GET request to https://api.stripe.com/v1/payment_intents/{id} using the stored secret key as HTTP Basic Auth credentials. If the response JSON contains status: succeeded, call $order->update_status('processing') and add an order note stating the payment intent ID and that the transition was made by the reconciliation check. Skip the order and log a notice via wc_get_logger() if the HTTP request fails or returns a non-200 status. ``` ## Where webhooks fail without leaving any trace WooCommerce registers a webhook endpoint on your site for Stripe to deliver payment events. Stripe sends a POST request to that URL whenever a payment event fires. If the request gets a 200 response, Stripe marks the delivery succeeded and moves on. If it gets anything else, Stripe retries on a schedule that backs off over time and eventually stops. The failure modes are quiet. A security plugin blocks the request with a 403 and writes nothing to WooCommerce logs. A server restart mid-checkout returns a 503 that Stripe retries, but by the time the retry lands the customer has long since left the page. The wrong endpoint URL after a site migration means every webhook has been failing since the domain changed. None of these create a visible error on the WordPress side. The only signal is an order that never moved. ## The payment intent ID already sitting in the pending order When the WooCommerce Stripe gateway initiates a payment, it stores the payment intent ID in the order meta before the customer's card is charged. That ID is a stable reference: it stays attached to the order whether the webhook arrives or not, and it is a direct key to the Stripe API. The endpoint GET /v1/payment_intents/{id}, called with the secret key as HTTP Basic Auth credentials, returns a JSON object with a status field. When status is succeeded, Stripe collected the money. When it is requires_action or processing, the payment is not yet complete and the order should stay where it is. The plugin makes exactly this call for each pending order that has the meta key set, then acts only on the ones where Stripe confirms success. ## A ten-minute window that keeps the check away from active checkouts An order enters Pending Payment the moment it is created, before the customer's browser has redirected back from Stripe. If the reconciliation check ran immediately, it would find orders sitting mid-checkout with a payment intent that might be processing or might be about to be cancelled. The plugin filters for orders created more than ten minutes ago. At that point, any webhook that was going to arrive on time has already arrived. The check runs every fifteen minutes via a WP-Cron event registered with wp_schedule_event(). For a store processing dozens of orders a day, that means at most a handful of Stripe API calls per run, well within rate limits. When the check finds a succeeded payment, it calls $order->update_status('processing'), which fires woocommerce_order_status_changed and sends the standard WooCommerce processing email to the customer. An order note records the payment intent ID and flags it as reconciled, so a subsequent run ignores it. ## Questions ### Will this send the WooCommerce confirmation email twice if the webhook arrives late? No. The reconciliation check queries only orders with Pending Payment status. If the webhook arrived and moved the order to Processing, that order is no longer in the query results. The confirmation email fires once, from whichever transition happens first. --- Source: https://steem.dev/blog/woocommerce-stripe-pending-order-reconciliation Build this plugin: https://steem.dev/app