A customer buys a digital download. Payment clears. The order lands on processing and stays there. No download email arrives. Ten minutes later you get a support ticket.
This is not a bug. It is what WooCommerce does by default for orders containing physical goods: wait in processing until a human ships them. The problem is that WooCommerce applies this same logic to virtual products, where there is nothing to ship and the customer expects instant access.
Why the built-in setting does not always work
WooCommerce has a setting under WooCommerce → Settings → Products → "Autocomplete orders" that automatically completes virtual-only orders. It works for straightforward cases. It does not work when:
- The order contains a mix of virtual and physical items
- The order is a subscription renewal processed by WooCommerce Subscriptions
- The completion needs to run on a delay or under a condition (for example, only above a certain order total)
- A payment gateway sends its webhook asynchronously, after the checkout has already completed
For stores selling memberships, courses, or mixed carts, the built-in setting covers maybe half the cases.
The functions.php approach and what it costs you
The common fix is a hook in functions.php:
// Common pattern — has problems, see below
add_action( 'woocommerce_thankyou', function ( $order_id ) {
$order = wc_get_order( $order_id );
if ( $order && $order->has_status( 'processing' ) ) {
$order->update_status( 'completed' );
}
} );
This fires on the thank-you page, not on payment confirmation. If a gateway sends its webhook asynchronously (Stripe, PayPal, most offline gateways), the order may still be pending when the thank-you page loads, so the hook does nothing. It also completes every order, including those with physical items.
A more correct version uses the status transition hook:
add_action( 'woocommerce_order_status_processing', function ( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order ) return;
foreach ( $order->get_items() as $item ) {
$product = $item->get_product();
if ( $product && ! $product->is_virtual() && ! $product->is_downloadable() ) {
return; // Physical item present — do not auto-complete
}
}
$order->update_status( 'completed', 'Auto-completed: virtual-only order.' );
} );
This is more reliable, but it still has gaps. Subscription renewals processed by WooCommerce Subscriptions fire woocommerce_order_status_processing like any other order — so you now need to check whether the order is a renewal and whether your subscription plugin expects to handle completion itself. That check looks different depending on which subscription plugin you use, and it changes when those plugins update.
The snippet works until it does not, and when it breaks, it breaks quietly.
The maintainable approach
Order Daemon handles this with a rule: when an order moves to processing, if all items are virtual or downloadable, complete it. No code, no maintenance surface, logged in the event timeline on every execution.
You can add conditions to handle the edge cases the snippet cannot: exclude subscription renewals, require a minimum order total, restrict to specific payment methods. When WooCommerce updates its internals, the rule keeps working because it runs through the WC data layer, not against status strings you are pattern-matching yourself.
The event log shows you every rule execution — what condition was checked, what it returned, what action ran. When an order does not auto-complete, the timeline tells you why instead of requiring you to add debug logging to your snippet.
Install the free plugin to set this up without writing or maintaining code. The developer’s map to the WooCommerce order lifecycle covers the full state machine if you want to understand the surrounding context. For a deeper look at why snippets like this accumulate into a maintenance problem, see the technical debt of custom order-completion code.