Payment shows as successful in Stripe or PayPal. The customer has a charge on their bank statement. Your WooCommerce order is still on pending. No fulfillment email was sent.
This is a webhook failure. The gateway sent a confirmation signal to your server, your server did not respond correctly, and the order never moved to processing. It is one of the most common order reliability problems on WooCommerce stores, and it affects every payment gateway.
Why webhooks fail
When a customer completes checkout, two things happen at the same time. Their browser is redirected to your thank-you page. The payment gateway sends a separate server-to-server request (a webhook or IPN) to a URL on your site, confirming that the payment was successful. WooCommerce listens at that URL and uses the confirmation to move the order from pending to processing.
The webhook is fire-and-forget. If your server cannot respond to it correctly, the order stays on pending and the gateway does not retry indefinitely. Common reasons the webhook fails to land:
- A caching plugin or server-level cache intercepts the webhook URL and returns a cached response instead of executing the PHP
- A security plugin or WAF blocks the gateway’s IP address, classifying the incoming request as suspicious
- A PHP error or brief database timeout at the moment the webhook arrives causes it to fail silently
- The gateway delays sending the webhook by several minutes, during which the customer may have placed another order or your server restarted
The failure produces no error visible to the customer and no alert to the store owner. The customer sees a successful payment. You see an order that never moved.
Finding affected orders
The correct way to query stuck orders is through the WooCommerce data layer, which works with both legacy post storage and HPOS:
$orders = wc_get_orders( [
'status' => 'pending',
'payment_method' => 'stripe',
'date_before' => gmdate( 'Y-m-d H:i:s', strtotime( '-1 hour' ) ),
'limit' => -1,
] );
foreach ( $orders as $order ) {
echo $order->get_id() . ' | ' . $order->get_payment_method_title() . "\n";
}
This finds pending orders placed more than an hour ago paid with Stripe. Adjust payment_method and date_before for your use case. For PayPal, the method slug is typically paypal. Run this as a WP-CLI eval or in a staging environment before acting on results.
Preventing recurrence
A webhook failure is a timing issue. The most reliable fix is not to depend on a single webhook. Using Action Scheduler, you can schedule a background verification task that checks the payment status directly with the gateway API for any order that has sat on pending beyond a threshold:
// Schedule a verification check 15 minutes after checkout for any pending order
add_action( 'woocommerce_checkout_order_created', function ( $order ) {
as_schedule_single_action(
time() + ( 15 * MINUTE_IN_SECONDS ),
'verify_pending_order_payment',
[ 'order_id' => $order->get_id() ]
);
} );
add_action( 'verify_pending_order_payment', function ( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order || ! $order->has_status( 'pending' ) ) return;
// Query the gateway API to check the actual payment status
// Implementation depends on the gateway — use its SDK or REST API
} );
This approach means a missed webhook does not produce a permanently stuck order. The background task catches it within minutes. It also means you have an audit trail: Action Scheduler logs every execution, so you can see which orders were verified and what the outcome was.
Note: Action Scheduler is triggered by WP-Cron by default. On low-traffic or fully cached sites, WP-Cron fires unreliably. Configure a real server-side cron to trigger the AS queue on a fixed schedule for anything handling payments.
The silent failure problem
Webhook failures that affect orders where the customer never complains are harder to catch. The customer assumes the download will arrive, or they forget about the purchase, or they write it off. These orders stay on pending, never get fulfilled, and show up only if someone manually audits order data.
The solution is proactive monitoring: a background process that surfaces any pending order older than a threshold, not waiting for a customer to report it. Order Daemon Pro provides CLI tools for auditing and replaying order events:
# List log entries filtered by status
wp odcm log list --status=error --format=table
# View the event timeline for a specific order
wp odcm log view --order-id=12345 --format=json --pretty
# Explain why a rule did not trigger for an order
wp odcm debug why-not --order=12345 --rule=5
For the full developer CLI reference, see the Order Daemon Pro CLI docs.
Install the free plugin to get the event log and checkout-failure detection without writing the verification logic yourself.