The Problem
A client messaged me on Saturday in a panic. Their store was live, customers were getting through to the checkout, entering an address, and then nothing. The shipping rates section in the WooCommerce Checkout Block sat completely empty. No "Free shipping", no "Flat rate", no error. Just a blank panel and a "Place Order" button that refused to submit.
I ran into this on a client project last month after a WooCommerce 9.6 update and the symptoms are always the same. The order summary loads, the address form accepts input, but the Store API call to /wc/store/v1/cart/select-shipping-rate never resolves to a usable list. Customers either bail or pick the only option visible (which is often nothing) and the checkout silently dies.
If your shipping rates worked yesterday and broke today, the cause is one of four things: a stale cart session token, a shipping zone that no longer matches the customer's country, a third-party plugin filtering woocommerce_package_rates to an empty array, or the Store API being blocked by a security plugin.
Why It Happens
The Checkout Block does not use the legacy wc_ajax endpoints. It hits the WooCommerce Store API directly at /wp-json/wc/store/v1/checkout and /wp-json/wc/store/v1/cart. These are unauthenticated REST endpoints that use a Cart-Token header to identify the session.
When the customer types an address, the block fires POST /wc/store/v1/cart/update-customer with the new shipping address. The server runs WC()->cart->calculate_shipping(), which loops through every active shipping zone, finds the one that matches the country and postcode, and returns the available methods. If any of these steps returns nothing, the rates panel is empty.
The most common breakages I see are:
- The
Cart-Tokencookie is missing or expired because a caching layer stripped theSet-Cookieheader on the first cart action. Every subsequent Store API call creates a new empty cart. - The shipping zone "Locations" field has a typo or uses the old country code (e.g.
UKinstead ofGB). The zone never matches. - A plugin like Advanced Shipping or Table Rate Shipping has a rule that returns
falsefromwoocommerce_package_rateswhen a condition is not met, leaving the array empty. - Wordfence or a similar security plugin has the Store API namespace on a block list, so the request returns 403 before WooCommerce even runs.
The Fix
Step 1: Confirm the Store API is reachable. Open DevTools, go to the Network tab, hit checkout, and watch for the call to /wp-json/wc/store/v1/cart/update-customer. You want to see a 200 response with a JSON body that contains a shipping_rates array.
- If the response is 403 or 401, a security plugin is blocking it. Whitelist the path.
- If the response is 200 but
shipping_ratesis[], your zones do not match. Move to Step 3. - If the response body is HTML of your homepage, your page cache is serving the wrong content for the Store API path.
Step 2: Exclude the Store API from page cache and security blocks. Most cache plugins already exclude /wp-json/, but Cloudflare "Cache Everything" rules do not. Add a bypass:
URL matches: example.com/wp-json/wc/store/*
Cache Level: Bypass
If you are running Wordfence, go to Wordfence → Firewall → All Firewall Options → Whitelisted URLs and add /wp-json/wc/store/v1/. Reload checkout and watch the Network tab again.
Step 3: Verify the shipping zone matches the test address. This catches more issues than anything else. Go to WooCommerce → Settings → Shipping → Shipping zones, click the zone that should match, and confirm the country list. Then run this in the browser console while on checkout to see what country code WooCommerce thinks the customer is in:
fetch('/wp-json/wc/store/v1/cart', {
headers: { 'Cart-Token': document.cookie.match(/wc_cart_token_[^=]+=([^;]+)/)?.[1] || '' }
})
.then(r => r.json())
.then(d => console.log(d.shipping_address));
If the country shows XX or empty, the address has not been saved to the session. Submit the address form once and rerun. If it shows the correct country but the zone still does not match, the zone is targeting a different region (Europe vs. European Union are not the same in the dropdown).
Step 4: Force-refresh the rates after a customer changes country. In some setups, the block caches the previous rates response and never refetches. Drop this into your child theme functions.php to invalidate the package hash on every customer update:
add_filter( 'woocommerce_shipping_packages', function ( $packages ) {
foreach ( $packages as $key => $package ) {
$packages[ $key ]['rate_hash'] = md5( wp_json_encode( $package ) . microtime() );
}
return $packages;
}, 999 );
This tells WooCommerce to never reuse a cached rate calculation for the current cart. It costs a small amount of extra processing on each address change, but on a checkout with five products and three zones it is negligible.
Step 5: Catch the empty package_rates filter. If a plugin is returning an empty array, find it with this debug snippet (remove after you identify the plugin):
add_filter( 'woocommerce_package_rates', function ( $rates, $package ) {
if ( empty( $rates ) ) {
error_log( 'Empty rates for package: ' . wp_json_encode( $package ) );
error_log( 'Active filters: ' . wp_json_encode( $GLOBALS['wp_filter']['woocommerce_package_rates']->callbacks ) );
}
return $rates;
}, 9999, 2 );
Check wp-content/debug.log after one checkout attempt. The active filters list will show every plugin that has hooked in. The WooCommerce Store API reference covers the full request shape if you need to dig further.
The Lesson
Empty shipping rates almost never means "WooCommerce is broken". It means the Store API call is hitting a wall before it can build the rates array. Check the Network tab first, confirm the cart token is being set, verify the zone matches, and only then start blaming filters. Skip these steps and you will spend two hours rewriting your shipping rules for nothing.
If your store is losing checkouts to empty shipping right now, I fix this kind of thing fast — see my services. For a related issue I covered last week, the WooCommerce Checkout Block payment methods fix walks through the same Store API debugging flow for payments.