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_feesand call$cart->add_fee( $name, $amount, $taxable, $tax_class ). - Wrap calculations in conditional checks using the
$cartinstance, 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.
- Standard Rate: Pass
trueas the third parameter and an empty string''as the fourth parameter. - Specific Tax Class: Pass
trueas the third parameter and the tax class slug (e.g.,'reduced-rate','zero-rate') as the fourth parameter. - Completely Tax-Exempt: Pass
falseas 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:
- 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 useif ( is_admin() && ! defined( 'DOING_AJAX' ) ) return;. - 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 exactly0, WooCommerce strips it from the display unless specifically styled or forced. - 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 thewoocommerce_checkout_create_order_fee_itemhook:
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
- WooCommerce developer docsWooCommerce developer docs
- WooCommerce documentationWooCommerce documentation
Drafted with AI assistance. Code targets current WordPress, WooCommerce and Next.js APIs; test changes on a staging site before production.