Shipping
This page shows a store administrator how to charge for delivery, using either one flat rate or shipping zones that offer methods based on the customer's address.
The two shipping engines
Shipping works the same way tax does. KahunaCart picks the engine for you.
| Zones configured | Engine | What it does |
|---|---|---|
| None | Flat rate | One amount on any order containing a physical item. |
| One or more | Zone | Address matching, method selection, per-class surcharges, free-shipping thresholds. |
Both engines write identical adjustment rows, so nothing downstream changes when you switch.
Note
Digital-only carts are skipped by both engines. If nothing in the cart is physical, no shipping is charged, no method is asked for, and checkout does not mention shipping.
Set a flat shipping rate
- In the admin, open Plugins → KahunaCart and the Commerce tab.
- Under Shipping, set Flat Shipping Rate to a decimal amount in the store currency (
shipping.flat_rate). - Set Flat Shipping Label to the name that appears on receipts (
shipping.label). - Click Save.
shipping:
flat_rate: '5.99' # decimal string in the store currency
label: 'Shipping'
The rate now applies to any order with at least one physical line, regardless of destination, item count or weight.
| Config key | Default | What it does |
|---|---|---|
shipping.flat_rate |
'0' |
The flat amount. 0 emits nothing. |
shipping.label |
'Shipping' |
The label on the receipt. |
Shipping zones
Shipping zones use the same geography matcher as tax zones: the same criteria, the same wildcard and range syntax, and the same "an empty criterion means any" rule. See Tax → Zones for the full matching semantics.
Zones are matched in the sort order you set in the admin, and the first matching zone wins. Which address is matched is decided by tax.basis, which governs both engines.
Create a shipping zone
- In the admin sidebar, click KahunaCart, then Shipping.
- Click New zone.
- Enter a Name, then set Priority to place this zone in the matching order.
- Fill in Countries, Regions and Postcodes. Values are comma separated, and an empty field matches anything.
- Click Add method, then enter a Label, choose a Type, and set Cost.
- Click Create zone.
Customers whose address matches the zone are now offered its methods at checkout.
Methods
A zone offers one or more methods, in three types.
| Type | Admin label | Behaviour |
|---|---|---|
flat |
Flat rate | Charges cost_minor, plus any per-class surcharges the cart triggers. |
free |
Free shipping | Always zero, whatever else is configured. Surcharges do not apply. |
pickup |
Local pickup | Charges cost_minor, usually zero, with no surcharges. |
| Field | Admin label | Notes |
|---|---|---|
label |
Label | What the customer sees, and what appears on the receipt. |
enabled |
Enabled | Disabled methods are hidden from customers but stay visible in the admin. |
cost_minor |
Cost | The base cost. |
per_class_json |
Per shipping class | A map of shipping class to surcharge in minor units. |
min_total_minor |
Free over | The free-shipping threshold. Blank means always offered. |
weight_rules_json |
Price by weight | Optional weight bands that replace cost_minor. |
position |
Position | The order methods appear in. |
Free-shipping thresholds
min_total_minor is the threshold a cart must reach before a method is offered at all. Give a free method named "Free shipping over $50" a threshold of 5000, and it stays out of the list until the cart clears it.
The threshold is tested against the items subtotal, not the total, so a discount that drops the subtotal below it removes free shipping.
Weight-based rates
A method can price by weight instead of charging one flat amount. Give it weight_rules, a list of bands:
weight_rules:
- { min_grams: 0, max_grams: 500, amount: '3.00' }
- { min_grams: 501, max_grams: 2000, amount: '6.50' }
- { min_grams: 2001, max_grams: ~, amount: '12.00' }
| Key | Admin label | Meaning |
|---|---|---|
min_grams |
From (grams) | Lower bound, inclusive. |
max_grams |
Up to (grams) | Upper bound, inclusive. Null or blank means open-ended. |
amount |
Cost | A decimal string in the store currency. |
Add a weight band
- In the admin sidebar, click KahunaCart, then Shipping.
- Click Edit on the zone, and find the method you want to price by weight.
- Under Price by weight, click Add weight band.
- Enter From (grams), Up to (grams) and Cost. Leave Up to (grams) blank for an open-ended top band.
- Repeat for each band, then click Save zone.
The method is now priced by weight, and cost_minor is no longer consulted.
How weight bands are resolved
- The order's weight is the sum of
qty × weight_gramsover its physical lines. Digital lines never contribute, and a line with no recorded weight contributes zero. - Weights come from each line's snapshot, never the live variant row, so correcting a product's weight does not change what a completed order was charged.
- The first matching band wins, in your order. Overlapping bands are resolved by position rather than refused.
- Bands replace the flat amount. Once a band matches,
cost_minoris not consulted. - A method whose bands do not cover the order is not offered. It disappears from the customer's list of options as if the zone did not offer it.
- A malformed band is dropped. A negative bound, a
max_gramsbelow itsmin_grams, or an amount that is not a plain decimal takes that band out of service. If every band is unusable, the method goes back to chargingcost_minor.
Everything else about the method still applies, in this order:
- The free-shipping threshold decides whether the method is offered at all.
- Weight bands decide whether it is offered, and what the base amount is.
- Per-class surcharges are added on top of that base.
- Tax is applied to the result if
apply_to_shippingis on for the matching rate.
A free method is the exception to step 2: a band cannot put a price back on it. Its bands still govern availability, which lets you offer free delivery on anything you can post.
Worked example
A UK zone with one method, "Royal Mail", carrying the three bands above and a fragile surcharge of 2.50:
| Cart | Weight | Result |
|---|---|---|
| 1 × 300 g mug | 300 g | Band 1 → 3.00. |
| 2 × 300 g mugs | 600 g | Band 2 → 6.50. |
2 × 300 g mugs, both fragile |
600 g | Band 2 → 6.50, plus the fragile surcharge once → 9.00. |
| 1 × 4 kg crate | 4000 g | Band 3 is open-ended → 12.00. |
| 1 × 300 g mug + 1 downloadable guide | 300 g | The download weighs nothing. Band 1 → 3.00. |
Add a second method, "Letter post", with one band 0–100 g at 1.20, and the 300 g cart never sees it. Add a third of type free with min_total_minor: 5000 and one band 0–2000 g, and it appears only once the subtotal reaches $50 and the parcel is under 2 kg.
Shipping classes and surcharges
Assign a shipping_class to a product, such as oversized, fragile or hazmat. The strings are yours. Give a method a surcharge for that class under Per shipping class.
- Surcharges apply per distinct class present in the cart, not per line and not per unit. A cart with two oversized items pays the oversized surcharge once.
- Classes come from each line's snapshot, never the live product row, so reclassifying a product does not change what a completed order was charged.
The method's total is cost_minor plus every triggered surcharge, and the adjustment records the breakdown so a receipt can show it.
Method selection at checkout
The customer's choice is recorded on the cart, but it is a hint rather than a guarantee. The selector re-checks it against what is currently available on every recalculation, and falls back to the first offered method rather than charging for something the store no longer offers.
Once zones exist, this makes checkout a two-step flow for physical orders:
- The customer fills in their address and submits. No method is chosen yet, so checkout redirects back.
- Checkout re-renders, and can now resolve the zone and show the real options at their real prices.
- The customer picks one and submits again.
Checkout blocks with a specific message for each case.
| Situation | Message |
|---|---|
| No country on the address | "Please enter your delivery address, including the country, so we can work out shipping." |
| Address is known but matches no zone | "We are unable to ship to that address. Please check it, or contact us for help." |
| Methods exist but none is chosen | "Please choose a shipping method." |
Omitting shipping_method_id from the POST means "not asked yet", and leaves whatever the cart already carries alone. Posting it empty clears the choice.
No service to an address
When a zone matches but offers this cart nothing, the calculation emits nothing at all. That happens when every method has a threshold the cart does not clear, or every method is disabled. It is not the same as free shipping: checkout blocks the order instead.
A method that is resolved always emits a row, including a zero-cost one, so "Free shipping — 0.00" appears as a line the customer can see.
The same applies when the address matches no zone: nothing is emitted, and checkout stops the order with the "unable to ship to that address" message. A method whose weight bands do not cover the order is unavailable through this same pathway, so if it was the zone's only method the customer gets that message rather than a free parcel.
API reference
All shipping endpoints require the kahunacart.settings permission.
GET /kahunacart/shipping/zones
POST /kahunacart/shipping/zones
PATCH /kahunacart/shipping/zones/{id}
DELETE /kahunacart/shipping/zones/{id}
Zone fields are name, countries, regions, postcodes and position. Methods are managed as part of their zone. The criteria accept PHP arrays or JSON arrays and are always stored as JSON.