Every WooCommerce order is an object moving through a defined state machine. Understanding this lifecycle, from the moment a customer clicks "Place Order" to the final completed status, is fundamental to building a store that does not silently lose orders.
For most developers, the lifecycle only becomes a focus when something breaks: a customer pays but gets no product, an order stalls in a status it should have left, or the support queue fills with manual fixes. These are not random bugs. They are predictable failure points in a system designed for physical goods, applied to use cases it was not designed for.
This post maps the whole lifecycle: the states, the transitions between them, the hooks available at each transition, and the points where things reliably go wrong.
The foundational assumption: physical goods first
WooCommerce’s default order lifecycle was built for physical goods. The processing status is a logical holding state for an order that needs to be picked, packed, and shipped. For a digital product, membership, or subscription, that same status is just an obstacle.
This legacy design is not a flaw; it is the source of most of the edge-case complexity developers deal with. Understanding it is the first step to managing it.
The core states
Every WooCommerce order exists in one of these states at any given time.
pending
The order exists and payment has not been confirmed. Stock is reserved but not reduced. Most orders start here.
How it gets here: the checkout process creates the order before payment is confirmed. Customers redirected to an off-site gateway (PayPal, Stripe redirect) land here first.
Common failure: the payment succeeds but the webhook confirming it never arrives. The order stays pending indefinitely. The customer has a charge, your store has nothing.
failed
Payment was declined, the customer abandoned the gateway, or the pending hold period expired. No stock was reduced.
processing
Payment confirmed. Stock reduced. Fulfillment emails sent. For physical goods, this means the order is ready to ship. For digital goods, this is often where the fulfillment gap opens.
How it gets here: a payment gateway sends an IPN or webhook confirming payment. WooCommerce receives it and transitions from pending.
on-hold
An administrative state. Stock is reduced (in most cases) but the order needs human review before proceeding.
How it gets here: usually set manually. Some gateways put orders on hold by default while a payment clears (cheque, bank transfer).
completed
Fulfilled. No further action required. For downloadable products, this status (along with processing) grants the customer download access.
How it gets here: manually for physical goods after shipping. Automatically for virtual-only orders if the setting is enabled, with caveats.
cancelled
Order stopped. Stock reversed if it was reduced.
refunded
Terminal state after a successful payment was reversed. WooCommerce creates a refund record linked to the order.
Custom statuses
For stores that need more granular workflow steps, WooCommerce supports custom statuses. Register them with WordPress’s register_post_status() and add them to the order system with the wc_order_statuses filter:
add_action( 'init', function () {
register_post_status( 'wc-awaiting-pickup', [
'label' => 'Awaiting Pickup',
'public' => true,
'show_in_admin_status_list' => true,
'show_in_admin_all_list' => true,
'exclude_from_search' => false,
] );
} );
add_filter( 'wc_order_statuses', function ( $statuses ) {
$statuses['wc-awaiting-pickup'] = 'Awaiting Pickup';
return $statuses;
} );
This works with both legacy post-based storage and HPOS.
The critical transitions
The state map is only useful if you understand where orders get stuck in transit.
pending to processing: the payment handshake
When a customer pays, their browser redirects to your thank-you page. At the same time, the payment gateway sends a separate server-to-server webhook confirming payment. The transition to processing depends entirely on your server receiving that webhook.
Webhooks are fire-and-forget. Common failure modes:
- A caching layer intercepts the webhook URL and returns a stale response
- A security plugin blocks the gateway’s IP address
- A PHP error or database timeout at the exact moment the webhook arrives causes it to fail silently
- The gateway itself delays sending the webhook, leaving the order in
pendingfor minutes or hours
The result: a customer with a bank charge and an order stuck on pending or flipped to failed.
processing to completed: the digital fulfillment gap
For digital products, memberships, and subscription activations, customers expect instant access after payment. WooCommerce provides a setting to auto-complete virtual-only orders, but it covers only the straightforward case. It does not handle mixed carts, subscription renewals, or orders where completion needs to be conditional.
Out of the box, a paid virtual order sits in processing until someone manually changes it. This is where the functions.php snippet accumulates. See WooCommerce orders stuck on Processing for the specific failure modes and how to handle them.
Key hooks for lifecycle management
These are the PHP action hooks you use to attach logic to the lifecycle:
woocommerce_payment_complete— fires on payment confirmation, before status moves toprocessing. Use for logic that must run immediately on confirmed payment.woocommerce_order_status_changed— fires on any status transition. Receives the order ID, old status, and new status.woocommerce_order_status_{status}— fires when an order enters a specific status.woocommerce_order_status_processingfires only on entry toprocessing.woocommerce_order_status_{old}_to_{new}— fires on a specific transition.woocommerce_order_status_pending_to_processingtargets only that path.
Architecting for reliability
A default WooCommerce installation assumes webhooks arrive and orders progress cleanly. A reliable one plans for the cases where they do not.
Asynchronous verification
Do not depend on a single webhook. Schedule a background verification task via Action Scheduler that re-checks the payment status with the gateway API for orders that have sat in pending beyond a threshold. This catches missed webhooks without impacting the checkout path.
Note: Action Scheduler is triggered by WP-Cron by default. On low-traffic or fully cached sites, WP-Cron is unreliable. Configure a real server-side cron to fire the Action Scheduler queue on a fixed schedule for anything mission-critical.
Conditional logic
Build rules that ask questions before acting: does this order contain only virtual products? Is it a renewal or a new subscription? Does it meet the order total threshold for manual review? A flat "complete all processing orders" rule breaks on mixed carts and subscription edge cases.
Auditing
When an automated process changes an order status, the order notes should record what ran, what it checked, and what it decided. Debugging a failed automation without a log is guesswork. With one, it is a thirty-second scan.
Managing the lifecycle actively
You have two realistic paths once you understand this map.
The first is to build the verification, conditional logic, and logging yourself using the hooks above and Action Scheduler. This works and gives you precise control. The maintenance surface is yours to own: every WooCommerce update is a potential breaking change for the logic you wrote.
The second is to use a tool that provides the rule engine, the event log, and the edge-case handling as maintained infrastructure. Order Daemon covers the automation, logs every execution on a per-order timeline, and handles the subscription and mixed-cart cases the simple snippet misses.
Either path is defensible. The one to avoid is the default: no verification, no conditional logic, and no audit trail, hoping webhooks arrive and orders complete themselves.