Skip to content

Powered by Grav

Coming soon — KahunaCart is in final testing. Join the list and be first to know. Join the list

Tax

This page shows a store administrator how to charge tax, using either one flat rate or tax zones matched against the customer's address.

The two tax engines

KahunaCart picks the engine for you. There is no switch to set.

Zones configured Engine What it does
None Flat rate One percentage, applied to every order.
One or more Zone Address matching, per-class rates, one adjustment per applicable rate.

The choice is made once per request, by checking whether any tax zone exists. Create your first zone and the flat rate stops being read. Both engines write identical adjustment rows of type tax, so receipts, refunds and the admin do not care which one produced them.

Set a flat tax rate

  1. In the admin, open Plugins → KahunaCart and the Commerce tab.
  2. Under Tax, set Flat Tax Rate (%) to the percentage (tax.rate).
  3. Set Flat Tax Label to the name that appears on receipts (tax.label).
  4. Turn on Flat Tax on Shipping if shipping belongs in the taxable base (tax.apply_to_shipping).
  5. Click Save.
YAML
tax:
  rate: '20'                # percent, as a string
  label: 'VAT'
  apply_to_shipping: true
  included: false

Every order is now taxed at that rate.

Config key Default What it does
tax.rate '0' Flat percent, as a string. 0 emits nothing.
tax.label 'Tax' The label on the receipt.
tax.apply_to_shipping false Whether shipping is part of the taxable base.
tax.included false Whether catalog prices already contain tax.
tax.basis shipping Which address decides the zone.

Note

Write rates as strings. Finland's 25.5% has to reach the cents intact, and a float does not do that reliably.

Zones

A zone is a jurisdiction. Zones are ordered by priority then id, and the first zone whose criteria all match the address wins. One zone applies per order, never a union.

A zone matches when every non-empty criterion matches.

Criterion Matching
countries ISO 3166-1 alpha-2, compared case-insensitively.
regions State or province name, compared case-insensitively after trimming. A name match, not a code match.
postcodes Patterns, matched after uppercasing and stripping spaces and hyphens from both sides.

Postcode patterns take three forms:

TXT
CB21AB          exact
CB2*            wildcard — '*' matches any run of characters
1000...1999     inclusive range

Ranges compare numerically when both ends and the candidate are all digits, and lexicographically otherwise. Because spaces and hyphens are stripped first, cb2 1ab, CB2-1AB and CB21AB are one postcode.

An empty or absent criterion means any, so a zone with no criteria at all is a catch-all for the end of the list. A missing address field is treated as an empty string, which matches only zones leaving that criterion empty.

Shipping zones use the same matcher and the same rules. The ordering differs: tax zones sort by priority, shipping zones by position.

Create a tax zone

  1. In the admin sidebar, click KahunaCart, then Tax.
  2. Click New zone.
  3. Enter a Name, then set Priority. Lower numbers are matched first.
  4. Fill in Countries, Regions and Postcodes. Values are comma separated, and an empty field matches anything.
  5. Under Rates, click Add rate, then fill in Label, Tax class, Rate %, Shipping and Priority.
  6. Click Create zone.

The zone engine is now active for the whole store, and the flat rate is no longer read.

Important

Saving a zone replaces that zone's rates with whatever the form holds. Add every rate the zone needs before you save.

Rates and tax classes

Field Notes
tax_class Defaults to standard. Matched against each product's tax_class.
rate Percent, as a string.
label What appears on the receipt, for example VAT, Sales Tax, GST.
apply_to_shipping 0 or 1. Set per rate, not per zone.
priority Application order within the zone.

A zone can carry one rate per tax class (standard, reduced, zero), and several rates on the same class where a jurisdiction stacks them, such as a state rate plus a county rate. Each rate produces its own adjustment row.

How the zone engine applies rates

  1. No country on the address, no tax. Nothing is emitted.
  2. No matching zone. Nothing is emitted.
  3. An item's tax class comes from the line's snapshot, never the live product row, so reclassifying a product does not change what a completed order was charged.
  4. Rates whose class has no items in the cart do not apply.
  5. Tax is charged on the discounted base. A €100 cart with a 20% coupon at 19% VAT is taxed on €80. A discount carrying an item id reduces that item's class; an order-level discount is apportioned across the classes by their share of the goods.
  6. Shipping is only taxed by rates with apply_to_shipping = 1. When several such rates apply, the shipping total is apportioned between them by their classes' share of the goods, so shipping is taxed exactly once.

Choose which address decides the zone

tax.basis selects the address that both the tax engine and the shipping engine match against. Set it to billing to match on the billing address instead.

Value Meaning
shipping (default) The destination principle, used by nearly every VAT and sales-tax regime.
billing Suits stores selling services.

The other address is the fallback when the chosen one has no usable country. A digital order has no shipping address, and a customer who filled in only one of the two still lands in a zone.

Inclusive vs exclusive pricing

tax.included applies to both engines and to the whole store. There is no per-product override.

Setting Catalog prices On the receipt
false (default) Pre-tax VAT (20%), added to the total.
true Already contain tax VAT (20% incl.), marked as included, does not move the total.

With tax.included set to true, the contained tax is back-calculated and recorded as an adjustment with included = 1. The customer pays the sticker price.

Apply the EU VAT preset

KahunaCart ships the standard VAT rate for all 27 EU member states. The import creates one zone per country, scoped to that country's ISO code, with one standard-class rate that also applies to shipping.

  1. In the admin sidebar, click KahunaCart, then Tax.
  2. Click Import EU VAT rates.

The admin reports how many zones were inserted. Zone names take the form VAT — Germany. The same import is available over the API:

TXT
POST /kahunacart/tax/presets/eu-vat

The import is idempotent by zone name. A country whose zone already exists is skipped entirely, rate and all. Rename a zone and a re-import creates a fresh zone for that country, because the name is the only thing it matches on.

Warning

The preset is a starting point, not tax advice. Rates change and nothing in KahunaCart tracks them. When a member state moves its rate, edit the zone yourself: a plugin update will not do it, and neither will a re-import.

Two things the preset does not cover:

  • Reduced rates. A store selling books, food, children's clothing or anything else on a reduced schedule has to add its own reduced-class rates, per country.
  • Thresholds and OSS registration obligations. Whether you must charge that VAT, and where you remit it, is between you and your accountant.
Where the shipped rates came from

The rates were verified on 2026-08-18 against the Tax Foundation's EU VAT table, VATupdate's global table and Eurofiscalis, cross-checked against national tax authorities for the four states that had moved recently: FI 24 → 25.5 in 2024, SK 20 → 23 in 2025, EE 22 → 24 in 2025, RO 19 → 21 in 2025. No member state changed its standard rate during 2026. The authoritative source is the Taxes in Europe Database.

Merchant-of-record providers

A merchant of record sells to your customer on your behalf, and it computes and remits sales tax, VAT and GST itself. Polar is the first-party example.

KahunaCart handles this for you. A provider declaring the MerchantOfRecord capability is charged a total with no local tax in it, whichever engine the store runs and whether or not prices are inclusive. There is nothing to configure.

The customer picks a payment provider at the end of checkout, after the cart was priced. The choice is stored on the cart, and the cart is recalculated before anything is charged. A recalculation replaces the order's adjustments wholesale, so switching between providers always ends with the total that matches the one about to charge.

Important

The cart page still shows the local tax estimate, because no payment provider has been chosen at that point. A shopper heading for Polar sees a tax line on the cart, no tax on the completed order, and Polar's own tax on their receipt.

Inclusive pricing with a merchant of record

An inclusive tax row never moves the total, so suppressing it changes no charged amount. What it stops is a receipt reporting tax the store did not collect and will not remit.

Your inclusive prices still contain the tax you built into them, and a merchant-of-record provider adds its own jurisdiction's tax to that gross number. KahunaCart does not re-derive net prices from your catalog.

Tip

A store selling mainly through a merchant of record should price net. Set tax.included to false.

API reference

All tax endpoints require the kahunacart.settings permission.

TXT
GET    /kahunacart/tax/zones
POST   /kahunacart/tax/zones
PATCH  /kahunacart/tax/zones/{id}
DELETE /kahunacart/tax/zones/{id}
POST   /kahunacart/tax/presets/eu-vat

Zone fields are name, countries, regions, postcodes and priority. The three criteria accept PHP arrays or JSON arrays, and are always stored as JSON, never null.