When you sell to both taxable customers and tax-exempt entities (like municipalities or government organizations), WooCommerce gives you the tools to define tax classes and zero-rated rules easily. But things get messy when you move to the new WooCommerce Checkout Blocks — especially if you rely on custom PHP logic to apply tax exemptions dynamically.
One of the most frustrating issues developers encounter is this:
The “I am tax-exempt” checkbox works initially, but when the customer adds an order note or switches payment methods, WooCommerce resets the tax back to default.
In this guide, we’ll go step-by-step to understand why this happens, and how to properly fix it using Store API hooks, ensuring that your exemption logic persists across AJAX calls in block-based checkout.
The Problem: Why Checkout Blocks Break Session Logic
If you’ve built your exemption feature like this:
- You added a checkbox during checkout for “I am tax-exempt.”
- You store the user’s selection in both session and customer meta.
- You apply the exemption using
WC()->customer->set_is_vat_exempt(true)during cart calculations.
Everything works fine… until the user interacts with the Checkout Block (like switching payment methods, updating address, or adding order notes).
That’s because the new Checkout Blocks use the WooCommerce Store API.
These AJAX calls (/wp-json/wc/store/v1/cart and /wp-json/wc/store/v1/checkout) are stateless — meaning they don’t trigger your legacy hooks such as:
woocommerce_cart_loaded_from_session
woocommerce_before_calculate_totals
These hooks only run when PHP reloads the entire checkout page, not during AJAX updates from the Store API.
As a result:
- The checkbox visually stays checked
- But taxes revert to non-exempt
because WooCommerce didn’t reapply your exemption logic during the AJAX call.
The Correct Way: Hook into the Store API Lifecycle
To fix this properly, we need to hook into the Store API lifecycle, not just traditional WooCommerce actions.
Step 1: Capture Tax Exemption Checkbox on Checkout
First, we’ll store the checkbox value whenever a checkout request is made via the Store API.
This ensures the data is updated instantly when the user toggles the checkbox, even during AJAX requests.
add_action('woocommerce_store_api_checkout_update_customer_from_request', 'handle_tax_exempt_update_on_ajax', 10, 2);
function handle_tax_exempt_update_on_ajax($customer, $request) {
$is_exempt = isset($request['additional_fields']['tax-exemption/claim-exemption'])
&& $request['additional_fields']['tax-exemption/claim-exemption'];
$customer->update_meta_data('_tax_exempt_checkout', $is_exempt ? 'yes' : 'no');
$customer->save();
$customer->set_is_vat_exempt($is_exempt);
if (WC()->session) {
WC()->session->set('tax_exempt_checkout', $is_exempt ? 'yes' : 'no');
}
}
This fires whenever the Store API updates customer details — which happens for every checkout action (even changing address or payment method).
Step 2: Reapply Tax Exemption for Every Store API Request
The next problem is persistence.
Even if we’ve stored the meta and session data, WooCommerce won’t automatically reapply it on each API call.
We can fix this by hooking into the woocommerce_store_api_cart_data action, which runs before WooCommerce recalculates cart totals for the Store API.
add_action('woocommerce_store_api_cart_data', 'apply_tax_exempt_during_store_api', 5, 1);
function apply_tax_exempt_during_store_api($cart_data) {
if (!WC()->customer) {
return $cart_data;
}
$is_exempt = WC()->customer->get_meta('_tax_exempt_checkout') === 'yes';
if (!$is_exempt && WC()->session) {
$is_exempt = WC()->session->get('tax_exempt_checkout') === 'yes';
}
WC()->customer->set_is_vat_exempt($is_exempt);
return $cart_data;
}
Now, no matter what triggers the AJAX refresh — notes, addresses, payment methods — WooCommerce will consistently reapply your tax exemption.
Step 3: Save and Clear Data on Order Completion
Finally, let’s persist the exemption state in the order for admin visibility and clear it after purchase.
add_action('woocommerce_checkout_create_order', 'save_tax_exemption_to_order', 10, 2);
function save_tax_exemption_to_order($order, $data) {
if (WC()->customer && WC()->customer->is_vat_exempt()) {
$order->update_meta_data('_is_tax_exempt', 'yes');
$order->update_meta_data('_tax_exemption_claimed', current_time('mysql'));
}
}
add_action('woocommerce_thankyou', 'clear_tax_exemption_after_order');
function clear_tax_exemption_after_order($order_id) {
if (WC()->customer) {
WC()->customer->delete_meta_data('_tax_exempt_checkout');
WC()->customer->save();
WC()->customer->set_is_vat_exempt(false);
}
if (WC()->session) {
WC()->session->set('tax_exempt_checkout', 'no');
}
}
And if you’d like to display this data in the admin panel:
add_action('woocommerce_admin_order_data_after_billing_address', 'display_tax_exempt_admin');
function display_tax_exempt_admin($order) {
$is_exempt = $order->get_meta('_is_tax_exempt');
if ($is_exempt === 'yes') {
echo '<p><strong>Tax Status:</strong> <span style="color: #2271b1;">✓ Tax Exempt (Municipal)</span></p>';
$claimed_date = $order->get_meta('_tax_exemption_claimed');
if ($claimed_date) {
echo '<p><small>Claimed: ' . esc_html(date('M j, Y g:i A', strtotime($claimed_date))) . '</small></p>';
}
}
}
Handling Payment Method Changes
If you still find that switching payment methods doesn’t immediately recalculate totals, you can add a small JS listener to trigger a cart refresh:
document.addEventListener('wc-blocks-checkout-payment-method-selected', () => {
fetch(window.wc?.routes?.checkout || '/?rest_route=/wc/store/v1/cart', {
method: 'GET',
credentials: 'include'
});
});
That ensures the updated tax state is fetched and recalculated instantly.
Final Thoughts
This pattern — using Store API hooks like woocommerce_store_api_cart_data and woocommerce_store_api_checkout_update_customer_from_request — is essential whenever you’re working with custom checkout fields and WooCommerce Blocks.
Relying solely on legacy WooCommerce hooks like woocommerce_before_calculate_totals or woocommerce_cart_loaded_from_session won’t cut it anymore, as the new checkout runs almost entirely through REST API requests.
So if your tax-exemption (or any dynamic cart field) keeps disappearing during checkout updates, this is the modern, reliable fix.