WooCommerce Checkout Shipping Rates Not Showing: Fix

WooCommerce Checkout Block hiding shipping rates at the address step? Fix the broken store API call, session handle, and shipping zone match with code.
WooCommerceWordPressCheckout
April 28, 20266 min read1085 words

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:

  1. The Cart-Token cookie is missing or expired because a caching layer stripped the Set-Cookie header on the first cart action. Every subsequent Store API call creates a new empty cart.
  2. The shipping zone "Locations" field has a typo or uses the old country code (e.g. UK instead of GB). The zone never matches.
  3. A plugin like Advanced Shipping or Table Rate Shipping has a rule that returns false from woocommerce_package_rates when a condition is not met, leaving the array empty.
  4. 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_rates is [], 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.

Back to blogStart a project