Sheraz AhmadSenior Web Developer
Let's talk

WooCommerce Add Custom Fee to Cart: Complete Guide

Learn how to add custom fees and surcharges to WooCommerce carts dynamically, including tax rules, payment method triggers, and AJAX checkout refreshes.

When you need to dynamically alter customer totals, learning how to handle a WooCommerce add custom fee to cart requirement using native hooks is the cleanest method. Whether applying a flat handling charge, fragile packaging surcharges, or payment gateway fees, calculating costs at the cart level ensures proper tax attribution and reporting. This guide walks through implementing conditional, taxable, and payment-driven fees using core WooCommerce APIs compatible with WooCommerce 9.x and High-Performance Order Storage (HPOS).

Quick answer

  • Hook into woocommerce_cart_calculate_fees and call $cart->add_fee( $name, $amount, $taxable, $tax_class ).
  • Wrap calculations in conditional checks using the $cart instance, WooCommerce session variables, or customer shipping/billing addresses.
  • Check is_admin() && ! defined( 'DOING_AJAX' ) early to prevent fees from breaking in the administrative dashboard while allowing AJAX checkout updates.
  • For fees dependent on checkout inputs (like payment method or custom fields), attach a tiny JavaScript listener to trigger $('body').trigger('update_checkout').

The fundamental fee API: woocommerce_cart_calculate_fees

WooCommerce provides a dedicated action hook designed specifically for injecting fees into the cart total calculation loop: woocommerce_cart_calculate_fees. It runs every time the cart recalculates totals, passes the WC_Cart object as its sole parameter, and supports multiple fees.

add_action( 'woocommerce_cart_calculate_fees', 'sa_add_flat_handling_fee', 20, 1 );

function sa_add_flat_handling_fee( \WC_Cart $cart ): void {
    if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
        return;
    }

    $fee_amount = 5.00;
    $cart->add_fee( __( 'Standard Handling', 'theme-domain' ), $fee_amount, true, '' );
}

The $cart->add_fee() method accepts four parameters: fee name (string), fee amount (float), whether the fee is taxable (bool), and the tax class (string, leave empty for standard rate).

Never mutate $cart->total or cart item prices directly to add surcharges. Altering core totals manually breaks tax calculations, coupon discount splits, and order reconciliation in HPOS and external reporting plugins.

Adding conditional cart fees based on cart state

Most real-world implementations require business logic: fee thresholds, item categories, or fragile item surcharges. You inspect the cart items or subtotal before executing $cart->add_fee().

Minimum spend or weight-based surcharges

This implementation applies an oversize logistics surcharge if the total cart weight exceeds a defined threshold:

add_action( 'woocommerce_cart_calculate_fees', 'sa_apply_heavy_weight_surcharge', 20, 1 );

function sa_apply_heavy_weight_surcharge( \WC_Cart $cart ): void {
    if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
        return;
    }

    $max_weight_threshold = 25.0; // kilograms or pounds depending on store settings
    $cart_weight          = (float) $cart->get_cart_contents_weight();

    if ( $cart_weight > $max_weight_threshold ) {
        $heavy_surcharge = 15.00;
        $cart->add_fee( __( 'Heavy Goods Surcharge', 'theme-domain' ), $heavy_surcharge, true, '' );
    }
}

Category or specific product rules

If you sell fragile items that require specialized cold-pack or padded packaging, scan the cart contents before applying the fee:

add_action( 'woocommerce_cart_calculate_fees', 'sa_apply_fragile_category_fee', 20, 1 );

function sa_apply_fragile_category_fee( \WC_Cart $cart ): void {
    if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
        return;
    }

    $target_category = 'fragile';
    $has_target_item = false;

    foreach ( $cart->get_cart() as $cart_item ) {
        if ( has_term( $target_category, 'product_cat', $cart_item['product_id'] ) ) {
            $has_target_item = true;
            break;
        }
    }

    if ( $has_target_item ) {
        $cart->add_fee( __( 'Special Handling (Fragile)', 'theme-domain' ), 10.00, true, '' );
    }
}

Taxes: handling standard, zero, and custom tax classes

Handling taxes correctly prevents legal issues and accounting disputes. The third argument of add_fee() toggles taxability, while the fourth argument identifies the tax class.

  1. Standard Rate: Pass true as the third parameter and an empty string '' as the fourth parameter.
  2. Specific Tax Class: Pass true as the third parameter and the tax class slug (e.g., 'reduced-rate', 'zero-rate') as the fourth parameter.
  3. Completely Tax-Exempt: Pass false as the third parameter.
add_action( 'woocommerce_cart_calculate_fees', 'sa_add_tax_exempt_donation', 20, 1 );

function sa_add_tax_exempt_donation( \WC_Cart $cart ): void {
    if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
        return;
    }

    // Check if the user selected a donation option in session
    $donation = (float) \WC()->session->get( 'customer_donation_amount' );

    if ( $donation > 0 ) {
        // Third parameter is false: exempt from sales tax
        $cart->add_fee( __( 'Voluntary Donation', 'theme-domain' ), $donation, false );
    }
}

When standard tax is enabled, WooCommerce calculates the fee's tax based on the store's tax calculation settings (tax based on customer shipping address, billing address, or store base).

How to add payment-method-based fees

Adding a fee for specific payment options (like Cash on Delivery or invoice factoring) requires reading the chosen payment gateway from the WooCommerce session. Because payment selections update via AJAX on the checkout page, you need both server-side logic and checkout events.

add_action( 'woocommerce_cart_calculate_fees', 'sa_fee_by_payment_method', 20, 1 );

function sa_fee_by_payment_method( \WC_Cart $cart ): void {
    if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
        return;
    }

    $chosen_gateway = \WC()->session->get( 'chosen_payment_method' );

    if ( 'cod' === $chosen_gateway ) {
        $cod_fee = 7.50;
        $cart->add_fee( __( 'Cash on Delivery Surcharge', 'theme-domain' ), $cod_fee, true, '' );
    }
}

To ensure the fee recalculates instantly when the user toggles payment radios on the checkout screen, ensure your theme or child theme enqueues this short DOM event listener:

jQuery(function($) {
    $('form.checkout').on('change', 'input[name="payment_method"]', function() {
        $('body').trigger('update_checkout');
    });
});

WooCommerce already listens for payment changes in core scripts, but themes that override checkout templates frequently decouple these triggers. Enqueueing this fallback guarantees that changing payment methods forces an AJAX fee update.

Custom checkbox options: toggle fees on the checkout page

When offering an optional service (such as gift wrapping) directly on the checkout form, register a custom field, process it into the customer session via AJAX, and evaluate that session key inside woocommerce_cart_calculate_fees.

Step 1: Render the checkout field

add_action( 'woocommerce_review_order_before_payment', 'sa_render_gift_wrap_field' );

function sa_render_gift_wrap_field(): void {
    $value = (bool) \WC()->session->get( 'enable_gift_wrapping' );

    woocommerce_form_field( 'enable_gift_wrapping', [
        'type'    => 'checkbox',
        'class'   => ['form-row-wide'],
        'label'   => __( 'Add Gift Wrapping (+$4.00)', 'theme-domain' ),
        'default' => $value ? 1 : 0,
    ], $value ? 1 : 0 );
}

Step 2: AJAX handler to store the choice in session

add_action( 'wp_ajax_sa_update_gift_wrap_session', 'sa_handle_gift_wrap_toggle' );
add_action( 'wp_ajax_nopriv_sa_update_gift_wrap_session', 'sa_handle_gift_wrap_toggle' );

function sa_handle_gift_wrap_toggle(): void {
    check_ajax_referer( 'sa_checkout_nonce', 'security' );

    $status = ! empty( $_POST['status'] ) && 'true' === $_POST['status'];
    \WC()->session->set( 'enable_gift_wrapping', $status );

    wp_send_json_success();
}

Step 3: Trigger the recalculation via JavaScript

Enqueued on the checkout page, this script captures the toggle, updates the session variable, and tells WooCommerce to refresh the fragments:

jQuery(function($) {
    $(document).on('change', '#enable_gift_wrapping', function() {
        const isChecked = $(this).is(':checked');

        $.post(saCheckoutParams.ajaxUrl, {
            action: 'sa_update_gift_wrap_session',
            security: saCheckoutParams.nonce,
            status: isChecked
        }, function() {
            $('body').trigger('update_checkout');
        });
    });
});

Step 4: Calculate the fee conditionally

add_action( 'woocommerce_cart_calculate_fees', 'sa_apply_gift_wrap_fee', 20, 1 );

function sa_apply_gift_wrap_fee( \WC_Cart $cart ): void {
    if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
        return;
    }

    if ( \WC()->session->get( 'enable_gift_wrapping' ) ) {
        $cart->add_fee( __( 'Gift Wrapping', 'theme-domain' ), 4.00, true, '' );
    }
}

Troubleshooting fees that disappear or calculate incorrectly

If custom fees fail to show up in the cart or checkout review tables, verify these three common failure points:

  1. Blocking AJAX inside admin guards: Calling if ( is_admin() ) return; kills fees during checkout because the checkout refresh request routes through /wp-admin/admin-ajax.php. Always use if ( is_admin() && ! defined( 'DOING_AJAX' ) ) return;.
  2. Zero or negative amounts: $cart->add_fee() supports negative values to provide dynamic discounts, but negative amounts cannot exceed the cart total. If a fee resolves to exactly 0, WooCommerce strips it from the display unless specifically styled or forced.
  3. Session variables wiped on order placement: When orders convert, session keys do not persist to the database automatically. If you need fee notes saved into order metadata in HPOS (wp_wc_orders_meta), record them using the woocommerce_checkout_create_order_fee_item hook:
add_action( 'woocommerce_checkout_create_order_fee_item', 'sa_save_fee_meta', 10, 4 );

function sa_save_fee_meta( \WC_Order_Item_Fee $item, string $fee_key, stdClass $fee, \WC_Order $order ): void {
    if ( 'Gift Wrapping' === $item->get_name() ) {
        $item->add_meta_data( '_service_sku', 'SRV-GW-01', true );
    }
}

Checklist

  • Hook registered strictly to woocommerce_cart_calculate_fees
  • Admin guard includes ! defined( 'DOING_AJAX' )
  • Added fees use float values for amounts (4.00, not string "4.00")
  • Taxable status (true/false) and tax class explicitly defined
  • Client-side DOM inputs trigger $('body').trigger('update_checkout') on change
  • Tested with both guest checkouts and logged-in users to verify session persistence
  • Compatible with HPOS and WooCommerce 9.x checkout pipelines
Reference documentation

Drafted with AI assistance. Code targets current WordPress, WooCommerce and Next.js APIs; test changes on a staging site before production.

Need a fast, modern website?

I'm Sheraz Ahmad, a senior WordPress and headless Next.js developer with 7+ years of shipping high-performance sites. Open to permanent roles, contracts and freelance projects.

Let's talk