WooCommerce Hook Reference for Developers: Actions & Filters You Actually Use

WooCommerce has hundreds of hooks. The official documentation lists them all, which sounds helpful until you’re staring at a wall of hook names trying to figure out which one fires where you actually need it. After building WooCommerce extensions for a few years, I keep coming back to the same couple dozen. This is a reference for those — organised by where in WooCommerce they fire, with examples you can paste directly into a plugin.

If you’re new to the WordPress hook system itself, the WordPress actions and filters guide covers the fundamentals before you read this.

Product page hooks

These fire on single product pages (single-product.php template). Most are self-explanatory from their names — they fire before or after specific elements on the page.

// Add content before the product title
add_action( 'woocommerce_single_product_summary', 'my_before_product_title', 4 );
function my_before_product_title(): void {
    echo 'New Arrival';
}

// Add content after the product price (priority 15 puts it after price at 10, before cart at 30)
add_action( 'woocommerce_single_product_summary', 'my_after_price_content', 15 );
function my_after_price_content(): void {
    global $product;
    if ( $product->is_on_sale() ) {
        echo '

Sale ends Sunday.

';
} } // Add a tab to the product tabs section add_filter( 'woocommerce_product_tabs', 'my_custom_product_tab' ); function my_custom_product_tab( array $tabs ): array { $tabs['shipping_info'] = [ 'title' => 'Shipping Info', 'priority' => 50, 'callback' => 'my_shipping_tab_content', ]; return $tabs; } function my_shipping_tab_content(): void { echo '

Shipping Information

';
echo '

Free shipping on orders over $50.

';
} // Modify the "Add to cart" button text add_filter( 'woocommerce_product_single_add_to_cart_text', 'my_add_to_cart_text' ); function my_add_to_cart_text(): string { return 'Buy Now'; }
PHP

The priority number on woocommerce_single_product_summary controls where your content appears in the stack. WooCommerce’s own elements use these priorities: title = 5, rating = 10, price = 10, excerpt = 20, add-to-cart = 30, meta = 40, sharing = 50. Set yours between those values to slot in where you need.

Shop and archive hooks

// Add content before the shop loop
add_action( 'woocommerce_before_shop_loop', 'my_before_shop_content', 20 );
function my_before_shop_content(): void {
    echo '
Summer sale — 20% off everything.
';
} // Add content to each product card in the loop (after the title) add_action( 'woocommerce_after_shop_loop_item_title', 'my_product_card_content', 15 ); function my_product_card_content(): void { global $product; echo 'SKU: ' . esc_html( $product->get_sku() ) . ''; } // Change the number of products per row add_filter( 'loop_shop_columns', 'my_shop_columns' ); function my_shop_columns(): int { return 4; // default is 3 or 4 depending on theme } // Change products per page add_filter( 'loop_shop_per_page', 'my_products_per_page' ); function my_products_per_page(): int { return 24; } // Remove the sorting dropdown above the shop loop remove_action( 'woocommerce_before_shop_loop', 'woocommerce_catalog_ordering', 30 );
PHP

Cart hooks

// Add a notice above the cart table
add_action( 'woocommerce_before_cart', 'my_cart_notice' );
function my_cart_notice(): void {
    $threshold = 50;
    $total     = (float) WC()->cart->get_subtotal();
    $remaining = $threshold - $total;

    if ( $remaining > 0 ) {
        wc_print_notice(
            sprintf( 'Add $%.2f more to get free shipping!', $remaining ),
            'notice'
        );
    }
}

// Add a custom fee to the cart
add_action( 'woocommerce_cart_calculate_fees', 'my_handling_fee' );
function my_handling_fee(): void {
    if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
        return;
    }
    WC()->cart->add_fee( 'Handling Fee', 2.50, true ); // true = taxable
}

// Validate cart item — prevent adding if a condition fails
add_filter( 'woocommerce_add_to_cart_validation', 'my_cart_validation', 10, 3 );
function my_cart_validation( bool $passed, int $product_id, int $quantity ): bool {
    $product = wc_get_product( $product_id );

    if ( $product && $product->get_stock_quantity() < $quantity ) {
        wc_add_notice( 'Not enough stock for that quantity.', 'error' );
        return false;
    }

    return $passed;
}

// Modify cart item data (adds custom data to the cart item)
add_filter( 'woocommerce_add_cart_item_data', 'my_cart_item_data', 10, 2 );
function my_cart_item_data( array $cart_item_data, int $product_id ): array {
    $cart_item_data['custom_note'] = 'Gift wrap requested';
    return $cart_item_data;
}
PHP

Checkout hooks

// Add a custom field to the checkout form
add_action( 'woocommerce_after_order_notes', 'my_checkout_field' );
function my_checkout_field( WC_Checkout $checkout ): void {
    woocommerce_form_field( 'custom_field', [
        'type'        => 'text',
        'class'       => [ 'form-row-wide' ],
        'label'       => 'Delivery instructions',
        'placeholder' => 'Any special instructions for delivery?',
    ], $checkout->get_value( 'custom_field' ) );
}

// Validate the custom field
add_action( 'woocommerce_checkout_process', 'my_checkout_field_validation' );
function my_checkout_field_validation(): void {
    // This field is optional — validate only if filled
    if ( ! empty( $_POST['custom_field'] ) && strlen( $_POST['custom_field'] ) > 500 ) {
        wc_add_notice( 'Delivery instructions must be under 500 characters.', 'error' );
    }
}

// Save the custom field to the order
add_action( 'woocommerce_checkout_update_order_meta', 'my_save_checkout_field' );
function my_save_checkout_field( int $order_id ): void {
    if ( ! empty( $_POST['custom_field'] ) ) {
        update_post_meta(
            $order_id,
            '_custom_field',
            sanitize_textarea_field( $_POST['custom_field'] )
        );
    }
}

// Display the saved field on the order admin screen
add_action( 'woocommerce_admin_order_data_after_billing_address', 'my_display_order_field' );
function my_display_order_field( WC_Order $order ): void {
    $value = get_post_meta( $order->get_id(), '_custom_field', true );
    if ( $value ) {
        echo '

Delivery instructions: ' . esc_html( $value ) . '

';
} }
PHP

Order hooks

// Fires when an order is placed (payment received or COD)
add_action( 'woocommerce_thankyou', 'my_order_placed', 10, 1 );
function my_order_placed( int $order_id ): void {
    $order = wc_get_order( $order_id );
    if ( ! $order ) return;

    // Log or trigger an external system
    error_log( 'New order: ' . $order_id . '' . $order->get_total() );
}

// Fires when order status changes
add_action( 'woocommerce_order_status_changed', 'my_order_status_change', 10, 4 );
function my_order_status_change( int $order_id, string $old_status, string $new_status, WC_Order $order ): void {
    if ( $new_status === 'completed' ) {
        // Award loyalty points, update CRM, etc.
    }
}

// Fires when payment is complete (most reliable hook for order completion)
add_action( 'woocommerce_payment_complete', 'my_payment_complete' );
function my_payment_complete( int $order_id ): void {
    $order = wc_get_order( $order_id );
    // Trigger fulfilment, send to warehouse, etc.
}

// Add a custom column to the orders list
add_filter( 'manage_woocommerce_page_wc-orders_columns', 'my_order_columns' );
function my_order_columns( array $columns ): array {
    $columns['custom_field'] = 'Delivery Notes';
    return $columns;
}

add_action( 'manage_woocommerce_page_wc-orders_custom_column', 'my_order_column_content', 10, 2 );
function my_order_column_content( string $column, WC_Order $order ): void {
    if ( $column === 'custom_field' ) {
        echo esc_html( $order->get_meta( '_custom_field' ) );
    }
}
PHP

Note the column hook names above — WooCommerce 7.1+ moved orders to a custom table (HPOS), so the column hooks use woocommerce_page_wc-orders rather than the old edit-shop_order. If you’re supporting older WooCommerce versions, you may need both.

Email hooks

// Add content to a specific WooCommerce email
add_action( 'woocommerce_email_after_order_table', 'my_email_content', 10, 4 );
function my_email_content( WC_Order $order, bool $sent_to_admin, bool $plain_text, WC_Email $email ): void {
    // Only add to customer emails, not admin notifications
    if ( $sent_to_admin ) return;

    $value = $order->get_meta( '_custom_field' );
    if ( $value ) {
        echo '

Delivery instructions: ' . esc_html( $value ) . '

';
} } // Modify email subject add_filter( 'woocommerce_email_subject_new_order', 'my_email_subject', 10, 2 ); function my_email_subject( string $subject, WC_Order $order ): string { return 'New order #' . $order->get_order_number() . '' . get_bloginfo( 'name' ); } // Add a recipient to the admin new order email add_filter( 'woocommerce_email_recipient_new_order', 'my_email_recipient', 10, 2 ); function my_email_recipient( string $recipient, WC_Order $order ): string { return $recipient . ',[email protected]'; } // Disable a specific WooCommerce email entirely add_filter( 'woocommerce_email_enabled_customer_processing_order', '__return_false' );
PHP

Price and tax hooks

// Modify product price programmatically
add_filter( 'woocommerce_product_get_price', 'my_dynamic_price', 10, 2 );
add_filter( 'woocommerce_product_get_regular_price', 'my_dynamic_price', 10, 2 );
function my_dynamic_price( string $price, WC_Product $product ): string {
    // Apply 10% discount to a specific category
    if ( has_term( 'members-only', 'product_cat', $product->get_id() ) && is_user_logged_in() ) {
        return (string) ( (float) $price * 0.9 );
    }
    return $price;
}

// Modify the displayed price HTML
add_filter( 'woocommerce_get_price_html', 'my_price_html', 10, 2 );
function my_price_html( string $price, WC_Product $product ): string {
    if ( $product->get_id() === 42 ) {
        return $price . ' (inc. VAT)';
    }
    return $price;
}
PHP

Be careful with price filters. They fire on every product render — in the loop, on single pages, in the cart, in admin. If your logic is expensive (a database query, an API call), cache the result. Doing a fresh query on every woocommerce_product_get_price call will noticeably slow down shop pages with many products. For how to query products efficiently, the WooCommerce product query guide covers the right approach.

Quick reference table

HookTypeWhere it fires
woocommerce_single_product_summaryActionProduct page summary area
woocommerce_product_tabsFilterProduct page tabs
woocommerce_product_single_add_to_cart_textFilterAdd to cart button text
woocommerce_before_shop_loopActionBefore shop/archive product grid
woocommerce_after_shop_loop_item_titleActionAfter each product card title in loop
loop_shop_per_pageFilterProducts per page count
woocommerce_before_cartActionBefore cart table
woocommerce_cart_calculate_feesActionWhen cart fees are calculated
woocommerce_add_to_cart_validationFilterBefore item added to cart
woocommerce_after_order_notesActionCheckout form, after order notes
woocommerce_checkout_processActionCheckout form validation
woocommerce_checkout_update_order_metaActionAfter order created at checkout
woocommerce_payment_completeActionAfter payment confirmed
woocommerce_order_status_changedActionWhen order status changes
woocommerce_email_after_order_tableActionWooCommerce emails, after order table
woocommerce_email_recipient_*FilterEmail recipient for each email type
woocommerce_product_get_priceFilterProduct price (every render)
woocommerce_get_price_htmlFilterDisplayed price HTML

Frequently asked questions

How do I find the right WooCommerce hook for what I need?

The WooCommerce source code is the most reliable reference. Install Query Monitor and enable its hooks panel — it shows every hook that fired on the current page load. For template hooks specifically, the WooCommerce GitHub repo has a woocommerce/templates/ directory where you can read exactly which hooks fire in each template file and in what order.

What is the difference between woocommerce_thankyou and woocommerce_payment_complete?

woocommerce_thankyou fires when the thank-you page loads — which happens every time someone visits that URL, including on page refresh. woocommerce_payment_complete fires once when payment is actually confirmed. For anything you only want to run once per order (sending to a fulfilment system, awarding points), use woocommerce_payment_complete. For page-level customisation on the thank-you screen, woocommerce_thankyou is fine.

How do I remove a WooCommerce hook added by WooCommerce itself?

Use remove_action() or remove_filter() with the same priority the hook was registered at. For example, to remove the related products section: remove_action( 'woocommerce_after_single_product_summary', 'woocommerce_output_related_products', 20 ). If the priority doesn’t match, nothing is removed — check the WooCommerce source to confirm the exact priority used. The WordPress actions and filters guide covers the remove pattern in detail.

Can I use WooCommerce hooks inside a custom plugin or only in functions.php?

A plugin is the right place — always. Hooks in functions.php are tied to the theme, so they disappear when the theme changes. Any WooCommerce customisation that your client or store depends on belongs in a plugin. For the WordPress plugin folder structure approach, keep WooCommerce-specific hooks in a dedicated file like includes/class-woocommerce-hooks.php rather than the main plugin file.

Do WooCommerce hooks work with High-Performance Order Storage (HPOS)?

Most do, but some legacy hooks that relied on post meta directly (using get_post_meta() on order data) may not fire correctly when HPOS is active. The safe approach is to always use WooCommerce’s order methods ($order->get_meta(), $order->update_meta_data()) rather than WordPress’s post meta functions directly. WooCommerce 7.1+ is HPOS-enabled by default on new installs, so this matters if you’re building extensions meant to be distributed.

About Me

Gemini_Generated_Image_6ed8rn6ed8rn6ed8

I’m Kamal, a WordPress developer focused on plugins, APIs, and scalable products.

Learn More