WooCommerce shipping zone matching: how WooCommerce picks a zone

Rama Adi
Written by

Rama Adi

Katelin Teen
Reviewed by

Katelin Teen

Last edited November 5, 2026

Expert Verified
Hand-drawn illustration of a store owner at a laptop beside a folded map with several outlined zones, a location pin in one zone and a parcel flying toward it
TL;DR

WooCommerce shipping zone matching is simple once you know the rule: it takes the customer's shipping country, state and postcode, checks your zones from the top of the list down, and the first match wins. City is never checked. A matched zone with no methods shows "no shipping options" instead of falling through, and US customers typing a ZIP+4 code miss exact and range postcode rules. So most "wrong shipping" tickets are a zone order or postcode format problem, and support can answer them from the address alone. eesel can explain that to shoppers if you write your zone rules down where it reads them.

How does WooCommerce match a customer to a shipping zone?

WooCommerce compares the customer's shipping address with each zone, in the order your zones are listed, and stops at the first one that fits. The shipping zones docs say it in one line: zones are matched "from the top of the list to the bottom", and a customer gets the methods from that one zone only.

I build integrations at eesel, including the WooCommerce one, and the fastest way I've found to stop arguing about what WooCommerce "should" do is to read the function that does it. Here it's get_zone_id_from_package() in WooCommerce core, and it does five things in order:

  1. Cleans up the address. Country and state are uppercased. The postcode is uppercased and every space and hyphen is removed, so cb2 3aa becomes CB23AA. The continent is worked out from the country. City and street lines aren't used at all.
  2. Checks the region. A zone passes if any one of its regions matches: the country, the state, or the continent. Continent, country and state have no ranking against each other.
  3. Checks postcodes. If a zone has postcode rules and none of them match, it's dropped. A zone without postcode rules is unaffected.
  4. Picks the first survivor. Zones are sorted by their position in the list, and the top one wins. If two zones share a position, the one created first wins.
  5. Falls back. If nothing matched, the customer gets zone 0, which the admin calls Rest of the world.

Only after that does WooCommerce load the zone's enabled methods. The matching step never looks at methods, which is the single most useful fact in this post, and I'll come back to it.

Hand-drawn flow: the shipping address country, state and postcode is checked against zones top to bottom, the first zone where region and postcode match wins and its enabled methods become the rates shown, with notes that no match means Rest of the world and a matched zone with no methods means no rates
Hand-drawn flow: the shipping address country, state and postcode is checked against zones top to bottom, the first zone where region and postcode match wins and its enabled methods become the rates shown, with notes that no match means Rest of the world and a matched zone with no methods means no rates

The zones screen says the same thing in its own words: a shopper can only be matched to one zone, and WooCommerce uses their shipping address to show the methods in that area.

The WooCommerce Shipping zones list with a Local zone for California and postcode 90210, offering Store pickup and Standard shipping, as taken from the WooCommerce docs on setting up shipping zones
The WooCommerce Shipping zones list with a Local zone for California and postcode 90210, offering Store pickup and Standard shipping, as taken from the WooCommerce docs on setting up shipping zones

If you're new to the rest of the store setup, my WooCommerce review, the Shopify vs WooCommerce comparison and WooCommerce pricing guide cover what you get in core before you add extensions.

What can a shipping zone match on?

Four things, and only four: continent, country, state and postcode. Here's what each one does in practice.

Zone regionHow it's matchedWatch out for
ContinentWorked out from the customer's country"Europe" includes the UK, so a UK address matches it
CountryExact country codeMust also be allowed under Shipping location(s)
StateCountry plus state, stored like US:CAOnly for countries where WooCommerce has a state list
PostcodeExact, wildcard * or range ...Narrows a zone, never widens it

Postcodes are the only way to go below state level. That's why city delivery zones trip people up: WooCommerce has no city field in the zone editor, and the city the customer types is ignored. One store owner in the UAE found every customer getting the Dubai rate:

Reddit

"In checkout, all customers are charged with the "Dubai" shipping method (since its the first option row in backend so wc prioritizes it) Is there a solution to this?"

With no postcodes to narrow by, every zone matched the country, and the first one won. The fix in that thread was a plugin that adds custom states.

The WooCommerce zone editor for a Local delivery zone, with California, United States as the region and the postcode 90210* entered, plus Free shipping and Local pickup methods, as taken from the WooCommerce shipping zones docs
The WooCommerce zone editor for a Local delivery zone, with California, United States as the region and the postcode 90210* entered, plus Free shipping and Local pickup methods, as taken from the WooCommerce shipping zones docs

The postcode box takes three formats, per the shipping zones docs and the matcher code:

  • Exact: 90210 or CB2 3AA. WooCommerce compares it with the customer's postcode, with and without the country's usual space or hyphen, so cb23aa still matches CB2 3AA.
  • Wildcard: a trailing *, like 902* or CB2*. It matches anything that starts with the text before it. A * in the middle (9*10) never matches.
  • Range: two values with three dots, like 90210...99000. Numeric ranges compare as numbers. The docs say plainly that wildcards in ranges, like 902*...990*, aren't supported.

A useful mental model: the region gets the customer into the zone, and postcodes can only keep them out. A zone with California and 902* matches a California address starting with 902, not every address starting with 902.

Why does a broad zone steal customers from a narrow one?

Because the first match wins, and a continent or country zone matches everything inside it. If "Europe" sits above "United Kingdom", a UK customer matches Europe first and never reaches the UK zone. This is the most common wrong-rate ticket I see in the r/woocommerce threads, and a steady source of ecommerce support contacts, and it takes days to find because both zones look right on their own.

Reddit

"I added all EU countries separately, instead of just the whole of Europe option, and it has fixed the issue. Thank you so much for your help! It was such a simple fix, yet I spent days and days trying to figure it out"

Two zone lists side by side: with Europe above United Kingdom a UK parcel matches Europe and gets the Europe rate, with United Kingdom on top it gets the UK rate
Two zone lists side by side: with Europe above United Kingdom a UK parcel matches Europe and gets the Europe rate, with United Kingdom on top it gets the UK rate

There are two fixes, and the order one is usually cleaner:

  1. Drag the narrow zone above the broad one. Local delivery first, then state, then country, then continent. The docs' own example list runs in that order.
  2. Or take the overlap out of the broad zone, like OutdoorG did by listing EU countries one by one instead of "Europe".

Since WooCommerce 11.1, the zones screen helps here. It flags a zone that an earlier zone fully covers, with this message:

This zone will not be matched because "%1$s" covers the same region earlier in the list. Move this zone above "%1$s" to make it available.

The catch is that it only compares continents, countries and states. If the earlier zone uses postcodes, it never warns. The troubleshooting docs are honest about this: no warning doesn't mean no overlap. So after any change, still test one address per zone.

Postcode zones can overlap too. One store split a city by postcode range across two zones, each with a different product's delivery method, and hit this:

Reddit

"The problem is, that the postcodes overlap in the two different zones. So if select variation B, and enter a postcode that exists in variation A's area, it says that shipping is not possible."

That's zone order again: the first zone won, and it had nothing for variation B. Zones are about where a customer is. If what you ship depends on the product, that belongs in shipping classes inside a zone, not in a second zone. The same "it depends on the product" confusion shows up in variation stock questions.

Why do US ZIP+4 codes miss postcode rules?

Because WooCommerce removes the hyphen. A customer who types 90210-1234 is matched as 902101234, which doesn't equal 90210 and isn't between 90210 and 99000 when compared as a number. The research for this post checked that against the postcode code in PHP 8: exact and range rules both miss, and only a wildcard like 902* or 90210* catches it.

A checkout field with 90210-1234 that WooCommerce reads as 902101234, missing an exact 90210 rule and a 90210...99000 range, and matching only the wildcard 902*
A checkout field with 90210-1234 that WooCommerce reads as 902101234, missing an exact 90210 rule and a 90210...99000 range, and matching only the wildcard 902*

The docs show a 902* example catching a ZIP+4 address, but they don't warn that exact and range rules won't. When they miss, the customer drops to the next zone down, usually your national or Rest of the world rate, and nobody gets an error. So the ticket arrives as "why did I pay $15 shipping when my neighbour paid $5?", not as a bug, and it lands in your ecommerce helpdesk looking like a billing question.

If you ship in the US, I'd use wildcards for any local zone, or check a few recent orders to see how often customers type the extra four digits.

Why does a customer see "no shipping options"?

Four causes cover nearly every case, and the first one surprises people most.

1. The zone matched but has no enabled methods. WooCommerce picks the zone first and loads its methods after. A zone with no methods, or only disabled ones, still wins, shows no rates, and does not send the customer on to the next zone or to Rest of the world. WooCommerce uses this on purpose: the docs suggest adding an empty zone to stop shipping to a region. That's handy, until someone toggles a method off for a day and forgets. A lost sale at checkout like that turns into pre-sales questions in your inbox.

2. The only method has a condition the cart doesn't meet. A zone with just free shipping and a $50 minimum shows nothing to a $40 cart. Same with weight brackets that leave a gap:

Reddit

"Have you set up shipping methods with weights? If so make sure your weight marry up. I can across this when methods had been set 0-5kg and then 5.5kg+ the weight of the product was between the two."

3. The country isn't allowed at all. Before any zone is checked, WooCommerce compares the country with Shipping location(s) in the general settings. If it's not allowed, only local pickup is calculated, whatever your zones say.

WooCommerce General settings with the Shipping location(s) dropdown open, showing Ship to all countries you sell to, Ship to all countries, Ship to specific countries only, and Disable shipping and shipping calculations, as taken from the WooCommerce shipping zones docs
WooCommerce General settings with the Shipping location(s) dropdown open, showing Ship to all countries you sell to, Ship to all countries, Ship to specific countries only, and Disable shipping and shipping calculations, as taken from the WooCommerce shipping zones docs

4. The address isn't finished yet. Checkout recalculates as the customer types, so the message can flash up mid-address. With Hide shipping costs until an address is entered ticked, no zone is matched until the country and any required state and postcode are filled. Real customers read this as "you don't ship to me":

Reddit

"if you entered the zip code, but have not entered the city yet, it will show "No shipping options available". Then after entering the city it will select the matching shipping method and the error message disappears. [...] we have had customers message us that they can't order because of it"

That setting can also catch logged-in customers with a saved address. One store owner on WooCommerce 10.7 saw shipping for logged-out customers but none for logged-in ones, and the fix was turning the setting off (thread).

The exact wording tells you which checkout the customer is on, which matters because the troubleshooting page only quotes the blocks version. These are the current strings in core:

WhereWhat the customer seesUsually means
Classic cart"No shipping options were found for [address]."Zone matched, no rates
Classic checkout"There are no shipping options available. Please ensure that your address has been entered correctly, or contact us if you need any help."Zone matched, no rates
Classic checkout"Unfortunately, we do not ship to [country]. Please enter an alternative shipping address."Country not in Shipping location(s)
Classic checkout"Please enter an address to continue."Address incomplete with costs hidden
Checkout block"No shipping options are available for this address. Please verify the address is correct or try a different address."Zone matched, no rates
Checkout block"Enter a shipping address to view shipping options."Address not complete yet
Checkout block"Sorry, we do not ship orders to the selected country"Country not allowed
Checkout block"Sorry, this order requires a shipping option."Placed with no rate picked

The classic strings come from cart-shipping.php and class-wc-checkout.php. On classic checkout you can soften the first two with the woocommerce_no_shipping_available_html and woocommerce_cart_no_shipping_available_html filters, which one commenter suggested to stop the panic (comment).

Does WooCommerce use the billing or shipping address?

The shipping address, always. The docs and the cart code agree. The billing address only matters when it's copied in: when the customer leaves Ship to a different address unticked, or when you pick Force shipping to the customer billing address. Then classic checkout copies every billing field into the shipping fields, and matching runs on those.

WooCommerce Shipping settings showing the calculation options, the Shipping destination choices with Default to customer billing address selected, and Enable debug mode, as taken from the WooCommerce shipping zones docs
WooCommerce Shipping settings showing the calculation options, the Shipping destination choices with Default to customer billing address selected, and Enable debug mode, as taken from the WooCommerce shipping zones docs

The support version: when a customer says "I changed my country and the price didn't change", ask which address they changed. Gift orders are the classic case, billing in one country and shipping to another.

Two more things on that screen are worth knowing. Enable the shipping calculator on the cart page doesn't apply to the Cart and Checkout blocks. And if you use the blocks version of local pickup, it isn't tied to a zone at all: it shows "regardless of customer location". The older local pickup method is a normal zone method and needs a zone match.

Why doesn't my fix show up straight away?

Usually it does. WooCommerce keeps each customer's calculated rates in their session, but the stored rates are keyed on the address, cart contents, coupons and a shipping settings version that changes when you save shipping settings. The zone lookup cache is reset when zones change too. So a stale rate after an edit usually comes from outside WooCommerce: the troubleshooting docs name page caching of Cart, Checkout and My Account as a cause.

To see what a test address matched, turn on Enable debug mode. It skips the stored rates and shows a notice like Customer matched zone "Local". Two quirks:

  • It doesn't show on checkout, during AJAX updates, or reliably in the Cart and Checkout blocks. The classic cart page is the easy place to read it.
  • When the fallback zone wins, the notice names it by its internal name, "Locations not covered by your other zones", not "Rest of the world". Same zone, different label.

When the zone is right but the rate still won't change with the country, check the checkout itself. One reply in a September 2026 thread suggested a test I'd copy:

Reddit

"I'd first try testing it with the classic WooCommerce checkout instead of the Checkout Block. If the shipping cost changes correctly there, then you can probably narrow it down to the Checkout Block or a plugin/theme compatibility issue."

Why doesn't a customer see free shipping?

Free shipping is a method inside one zone, so the customer has to match that zone first. A customer in Rest of the world won't see the free shipping you set up in "United States", however big their cart.

Then the method's own rule has to pass. Per the free shipping docs, it can need nothing, a coupon, a minimum amount, a minimum or a coupon, or both. The minimum is measured after coupon discounts unless you tick Apply minimum order rule before coupon discount. So a $55 cart with a $10 code is a $45 cart for free shipping, and that's a ticket. It's the shipping cousin of the traps in my WooCommerce coupon restrictions guide.

Last, Hide shipping rates when free shipping is available has been a core setting since WooCommerce 9.9. When free shipping qualifies, it keeps free shipping and local pickup and removes everything else. Handy, but if a customer asks why express shipping vanished once their cart passed $50, that's why. (The separate hide other methods doc still says you need code or a plugin. You don't, on 9.9 or later.)

Zones only decide the rate before purchase. After it, the tickets become "where is my order", which my posts on WISMO automation, WooCommerce guest order tracking and AI for shipping delay notifications cover.

How should support check a shipping zone ticket?

Ask for three fields, then walk the list. Every zone answer comes from the shipping country, state and postcode, so you can answer most tickets without logging in as the customer.

StepWhat to ask or checkWhy
1Shipping country, state and postcode, exactly as typedThat's all matching uses. City doesn't count
2Did they tick "Ship to a different address"?If not, their billing address was used
3The exact message, word for wordTells you classic or blocks, and which cause
4Walk your zones top to bottomThe first zone whose region and postcode fit is the answer
5Does that zone have an enabled method this cart qualifies for?Empty or conditional zones show nothing
6Test the address with debug mode on the classic cart pageConfirms the zone, skips stored rates

The answer is nearly always a lookup, and a good one to save as one of your shipping issue macros.

What isn't a lookup is the make-good: refunding the difference when a customer was overcharged, or waiving shipping on a reship. That's a money decision, like any refund request, and it should go to a person through a proper handoff.

That's how the stores I work with draw the line. Rúngne, a Norwegian climbing apparel brand that ships US orders from a US warehouse and everything else from Norway, runs eesel on Gorgias and Shopify across 2,100 tickets a month. Its draft agent checks stock at the right warehouse, but a delivery problem gets an apology and an escalation, never a guess about where the parcel is. Koffer.ch, a Swiss luggage shop on Freshdesk, points eesel at its delivery page alongside its terms and FAQ, so shipping answers come from what the shop published. Neither runs WooCommerce, and the setup carries over unchanged: write the rule down, let the agent explain it, keep the money with a person. My approval workflows guide covers where to set that line.

How eesel handles WooCommerce shipping questions

eesel connects to WooCommerce through the free eesel AI plugin or a REST API key, and answers in your chat bubble, your helpdesk (for example Gorgias on WooCommerce) or Slack. For shipping zone tickets, here's what it can and can't see, per the WooCommerce integration docs:

  • Look up orders reads an order "with its items, totals, status and notes". After purchase, that's how it checks what a customer was charged and where the order is, alongside the order status.
  • Look up products and categories reads your catalogue live, so it can tell a shopper what's in stock before they buy.
  • Your zone rules come from your writing. The documented sources are products, orders and customers, not shipping zones. eesel's own docs say written content such as your shipping policy isn't in WooCommerce, so connect your website. I'd write each zone in plain words on your shipping page: which postcodes get local delivery, the free shipping minimum and whether it's before or after coupons, which countries you don't ship to. Without that, the agent can't know your rules, and it shouldn't guess. That's the same hallucination guardrail I'd set for any policy question.
  • It can't see the cart or the zone screen. If a shopper says "it shows no shipping options", the agent can ask for their country, state and postcode, check them against the rules you wrote, and tell them which zone that is or that you don't ship there.
The eesel dashboard chat answering a request for recent WooCommerce orders, grouping them into tables of pending and processing orders
The eesel dashboard chat answering a request for recent WooCommerce orders, grouping them into tables of pending and processing orders

When a shopper needs a person, for a shipping refund say, the agent can leave a private order note with the address they gave and the rule it applied, so whoever picks it up doesn't start over. With Slack connected, that note waits for approval in the thread with Approve and Deny buttons.

If you'd rather set this up from a terminal, the eesel CLI lets Claude Code or Cursor run eesel integrations connect woocommerce to print the connect link, upload your written zone rules and shipping policy as knowledge, and ask test questions like "do you deliver locally to 90210-1234?" to check the answers before anything goes live. A store admin still opens that link and approves the connection on WooCommerce's own screen.

Where eesel isn't the right fit: if your rates come from a live carrier plugin or a table rate extension with dozens of rules, the agent only knows what you write down about them, so it can explain the policy but not quote an exact rate. And a store that ships one country at one flat rate probably doesn't need AI for this at all. For a wider view, see my WooCommerce chatbot guide, how to add AI to WooCommerce, and my roundup of WooCommerce AI options.

Try eesel on your WooCommerce shipping tickets

Want "why can't I check out?" answered with the actual zone rule instead of "please check your address"? eesel is an AI helpdesk teammate, one of the top WooCommerce support tools, that plugs into WooCommerce in a few minutes, reads the order and your catalogue, learns your shipping rules from your site, and tells the shopper what their address matched. Shipping refunds stay with your team, the same way a manual refund does. The free plan includes 100 credits, and paid plans start at $299 a month on the pricing page.

A WooCommerce coffee storefront with the eesel chat bubble open, where the agent recommends three coffees as product cards with prices after looking up products and checking stock
A WooCommerce coffee storefront with the eesel chat bubble open, where the agent recommends three coffees as product cards with prices after looking up products and checking stock

Try eesel on your store, then run it against last month's shipping tickets before it talks to a shopper.

Frequently Asked Questions

How does WooCommerce shipping zone matching work?
WooCommerce takes the customer's shipping country, state and postcode, then checks your zones from the top of the list to the bottom. The first zone whose region matches, and whose postcode rules match if it has any, wins. The customer sees that zone's enabled shipping methods and nothing from any other zone. If no zone matches, they get your Rest of the world zone. My WooCommerce review covers the rest of the platform.
Can a customer match more than one WooCommerce shipping zone?
No. WooCommerce sorts matching zones by their order in the list and takes only the first one. If two zones have the same order, the one created first wins. So a broad zone like Europe placed above a narrower zone like United Kingdom will catch UK customers, and the UK zone will never be used.
Why does WooCommerce say there are no shipping options available?
Usually the address matched a zone that has no enabled methods, the cart doesn't meet the condition on the only method in that zone (a free shipping minimum, say), or the address isn't complete yet. A zone with no methods doesn't pass the customer on to the next zone, it just shows nothing. Also check that the country is allowed under Shipping location(s) in your general settings. For the tickets this causes, see my guide to AI for pre-purchase questions.
How do postcode ranges and wildcards work in WooCommerce shipping zones?
A trailing * matches any postcode that starts with the text before it, so 902* matches 90210. Three dots make a range, like 90210...99000, which compares numbers. Wildcards inside ranges aren't supported. US customers who type a ZIP+4 code such as 90210-1234 miss exact and range rules, because WooCommerce reads it as 902101234, so use a wildcard if your customers type ZIP+4.
Does WooCommerce use the billing or shipping address to pick a zone?
The shipping address. When a customer doesn't tick "Ship to a different address", or the store forces shipping to the billing address, WooCommerce copies the billing fields into the shipping fields and matches on those. A shopper who changes only their billing country won't see their shipping rate change.
Can I set up a WooCommerce shipping zone for a city?
Not by city name. WooCommerce shipping zone matching only uses country, state, continent and postcode, and never reads the city field. To target a city, list its postcodes, or use a plugin that adds custom regions. Write down what you set up in your internal knowledge base so support can explain it.
How do I see which shipping zone a customer matched?
Turn on Enable debug mode under WooCommerce > Settings > Shipping > Shipping settings. It skips the stored rates and shows a Customer matched zone notice on a normal page load, such as the classic cart page. It doesn't show during checkout, AJAX updates or in the Cart and Checkout blocks, so test there with care, and turn it off afterwards.

Share this article