
How to Set Up Shipping Zones in WooCommerce?
Shipping zones are how WooCommerce decides which shipping methods and rates to show a customer based on where they're ordering from. Get zones wrong and you'll either quote a flat domestic rate to an international customer (eating the cost yourself) or, worse, show no shipping options at all and lose the sale entirely at checkout.
This guide covers how WooCommerce actually evaluates zones (order matters more than people expect), building zones for domestic, regional, and international shipping, choosing between flat rate, free shipping, and calculated methods, and registering a fully custom shipping method in code for rates that don't fit any of the built-in options.
How WooCommerce Evaluates Shipping Zones
A shipping zone is a named geographic region (a country, a set of states, or "everywhere else") paired with one or more shipping methods and their rates. When a customer enters an address at checkout, WooCommerce checks their location against your configured zones from top to bottom and uses the first matching zone, ignoring any zones below it, even if they'd also technically match.
This ordering detail is the single most common source of shipping misconfiguration: a broad "Rest of World" zone placed above a specific "Canada" zone will always win for Canadian customers, since WooCommerce stops checking once it finds a match.
Step 1: Create Your First Shipping Zone
Go to WooCommerce → Settings → Shipping → Add shipping zone. Give it a name (e.g., "Domestic"), and under Zone regions, select the countries or states it should cover. WooCommerce also supports matching by postcode, useful for zones scoped to a specific metro area.
Step 2: Add Shipping Methods to the Zone
Inside the zone, click Add shipping method and choose from WooCommerce's built-in options:
- Flat rate — a fixed cost, optionally varying by shipping class (useful if some products are heavier or bulkier and should cost more to ship).
- Free shipping — can be unconditional, or tied to a minimum order amount or a valid free-shipping coupon.
- Local pickup — no shipping cost at all; the customer collects the order in person.
Click into each method's settings to configure its cost. Flat rate supports a cost expression that references the cart, for example 10 + ( [qty] * 2 ) charges a $10 base fee plus $2 per item, evaluated automatically by WooCommerce at checkout.
Step 3: Order Your Zones Correctly
Back on the main Shipping settings screen, drag zones into the right order using the handle on the left of each row: most specific first, most general last. A typical setup looks like:
- Local pickup zone (your own city or postcode) — customers here see a pickup option and possibly a discounted local delivery rate.
- Domestic zone (your home country) — standard flat-rate or calculated shipping.
- Nearby region zone (a group of neighboring countries, if relevant) — a different rate structure than domestic.
- Rest of World (no country restrictions selected) — this always goes last, acting as the catch-all for anywhere not covered above.
Step 4: Use Shipping Classes for Rate Variation Within a Zone
If some products cost meaningfully more to ship (large, heavy, or fragile items) but you don't want to build an entirely separate zone for them, Shipping classes let you vary the rate within the same zone. Create classes under WooCommerce → Settings → Shipping → Shipping classes, assign them to individual products under each product's Shipping tab, and then set a different Flat rate cost per class inside each zone's Flat rate method settings.
Step 5: Register a Custom Shipping Method in Code
Sometimes none of the built-in methods fit, for example, a rate that depends on a per-product custom field, or an external carrier API you need to call for a live quote. WooCommerce's shipping system is built to be extended by registering your own class that extends WC_Shipping_Method. Add this to a site-specific plugin (custom shipping methods should not live in a theme, since switching themes would silently remove your store's shipping options):
add_action( 'woocommerce_shipping_init', function () {
if ( class_exists( 'WC_Distance_Based_Shipping' ) ) {
return;
}
class WC_Distance_Based_Shipping extends WC_Shipping_Method {
public function __construct( $instance_id = 0 ) {
$this->id = 'distance_based_shipping';
$this->instance_id = absint( $instance_id );
$this->method_title = __( 'Distance-Based Shipping', 'textdomain' );
$this->method_description = __( 'Charges a base rate plus a per-mile surcharge for orders over a set weight.', 'textdomain' );
$this->supports = [ 'shipping-zones', 'instance-settings' ];
$this->init();
}
public function init() {
$this->init_form_fields();
$this->init_settings();
$this->title = $this->get_option( 'title', __( 'Distance-Based Shipping', 'textdomain' ) );
$this->base_cost = $this->get_option( 'base_cost', 8 );
$this->per_lb = $this->get_option( 'per_lb', 0.5 );
add_action( 'woocommerce_update_options_shipping_' . $this->id, [ $this, 'process_admin_options' ] );
}
public function init_form_fields() {
$this->instance_form_fields = [
'title' => [
'title' => __( 'Method title', 'textdomain' ),
'type' => 'text',
'default' => __( 'Distance-Based Shipping', 'textdomain' ),
],
'base_cost' => [
'title' => __( 'Base cost', 'textdomain' ),
'type' => 'number',
'default' => 8,
],
'per_lb' => [
'title' => __( 'Cost per pound', 'textdomain' ),
'type' => 'number',
'default' => 0.5,
],
];
}
public function calculate_shipping( $package = [] ) {
$weight = 0;
foreach ( $package['contents'] as $item ) {
$product = $item['data'];
$weight += (float) $product->get_weight() * $item['quantity'];
}
$cost = (float) $this->base_cost + ( $weight * (float) $this->per_lb );
$this->add_rate( [
'id' => $this->id . $this->instance_id,
'label' => $this->title,
'cost' => $cost,
] );
}
}
} );
add_filter( 'woocommerce_shipping_methods', function ( $methods ) {
$methods['distance_based_shipping'] = 'WC_Distance_Based_Shipping';
return $methods;
} );
A few details worth understanding:
woocommerce_shipping_initis the correct hook to define the class on, sinceWC_Shipping_Method(the parent class) isn't loaded yet earlier in the request.woocommerce_shipping_methodsis the filter that registers your class so it appears in the "Add shipping method" list inside any zone, exactly alongside Flat rate and Free shipping.calculate_shipping()is where the actual rate logic runs;$package['contents']gives you the full cart contents for that shipment, letting you calculate cost based on weight, product count, or any custom product meta you want to factor in.- Once registered, this method needs to be added to a zone through the admin UI, exactly like any built-in method, it doesn't apply itself automatically.
Testing Your Zone Setup Before Launch
Zone misconfigurations rarely show up until a real customer from an unexpected location hits checkout, so it's worth deliberately testing edge cases rather than only checking the zone you built most recently:
- Test an address in every zone, not just the one you just edited. Use the cart's shipping calculator (or a full test checkout) with addresses in your domestic zone, any regional zone, and whatever falls into your catch-all "Rest of World" zone.
- Test a state or country that sits right at a zone boundary. If you have a regional zone covering specific states and a domestic zone covering the rest of the country, deliberately test one state from each side of that boundary to confirm neither is misclassified.
- Re-test after adding a new zone. Inserting a new zone above an existing one can silently change which zone an address that used to match the old one now falls into, since WooCommerce always stops at the first match from the top.
- Combine with a coupon test if you offer free-shipping coupons, since the free shipping method's "requires a valid free shipping coupon" setting depends on both the zone and the coupon being configured consistently.
Frequently Asked Questions (FAQ) About WooCommerce Shipping Zones
Almost always a zone ordering issue: WooCommerce uses the first zone (top to bottom) that matches the customer's address, so a broader zone placed above a more specific one will win incorrectly. Reorder your zones so the most specific regions sit at the top and the broadest catch-all zone sits at the bottom.
Yes, and it's common to offer more than one, for example both Flat rate and Free shipping (conditional on a minimum spend) in the same zone, letting the customer choose at checkout.
If no zone matches (including a catch-all "Rest of World" zone with no location restrictions), WooCommerce shows no shipping options at checkout, effectively blocking the order. Always keep an unrestricted catch-all zone at the very bottom of your zone list to avoid this.
Shipping classes work within a zone's shipping methods rather than being a separate zone concept; a single Flat rate method inside one zone can charge different costs depending on which shipping class a cart's products belong to, which is configured inside that method's own settings.
Yes, the Free shipping method has a setting for requiring a minimum order amount, a valid free-shipping coupon, or either condition, configured directly in that method's settings inside the zone.
Not in core; live carrier rate calculation requires an official extension (like WooCommerce Shipping, or dedicated UPS/FedEx/USPS integrations) added as a shipping method inside a zone, the same way Flat rate or Free shipping is added.
No, a single zone can include as many countries as you want with identical rates, so group countries with the same shipping cost together into one zone rather than creating a new zone for every individual country.
Conclusion
Shipping zones are simpler than they first appear once you internalize the one rule that governs everything else: WooCommerce checks zones top to bottom and stops at the first match, so ordering from most specific to most general is what makes the whole system behave predictably. Get the built-in Flat rate, Free shipping, and Local pickup methods configured correctly first, layering in shipping classes wherever certain products genuinely need a different rate.
For anything the built-in methods can't express, extending WC_Shipping_Method directly, as shown above, is the same real extension point WooCommerce's own official shipping extensions are built on, so it's a reliable pattern to reach for rather than a workaround.
Shipping is only one part of what a customer sees at checkout; tax calculation runs alongside it, and the tax settings guide is the natural next step once your zones are in place.


