Tax
Tax zones, the rates hanging off them, the rate CSV, price preferences and the tax engines a zone can be bound to.
Tax zones and the rates hanging off them, the CSV that carries the whole rate table in and out, the preferences that say whether a region or a currency is priced inclusive of tax, and the engines a zone can be quoted by instead. See Tax.
List tax zones
/api/v1/kahunacart/tax/zones
Every tax zone with its country, region and postcode criteria decoded, its priority, and the rates hanging off it. A zone is never returned without its rates, because a jurisdiction with no rate charges nothing. Rate percentages are decimal strings so figures like 25.5 reach the cents intact.
Create a tax zone
/api/v1/kahunacart/tax/zones
Create a tax zone and its rates in one call. A name is required, and every rate needs a percentage. An empty criterion list matches everything, so a zone with no countries is the fallback zone. States are matched by ISO 3166-2 code or by name for the countries that publish a list, so "Colorado", "CO" and "US-CO" all match the same address.
Parameters
| Name | Type | Description |
|---|---|---|
| name required | string | What the merchant calls this jurisdiction |
| countries optional | array of string | ISO country codes; empty matches every country |
| regions optional | array of string | States or provinces, as an ISO 3166-2 code or a name |
| postcodes optional | array of string | Postcodes or prefixes; empty matches every postcode |
| priority optional | integer | Higher priority zones win when several match |
| rates optional | array of object | The rates this zone charges. Keys: `rate`, `label`, `tax_class`, `apply_to_shipping`, `priority`, `translations` |
Update a tax zone
/api/v1/kahunacart/tax/zones/{id}
Change a zone's fields. Sending rates replaces the zone's whole set of rates and gives them new ids, so return each rate's translations unchanged if you want the overrides to survive. Omitting rates leaves them standing.
Parameters
| Name | Type | Description |
|---|---|---|
| id required | integer | Tax zone id |
| name optional | string | |
| countries optional | array of string | |
| regions optional | array of string | |
| postcodes optional | array of string | |
| priority optional | integer | |
| rates optional | array of object | Replaces the whole set when present. Keys: `rate`, `label`, `tax_class`, `apply_to_shipping`, `priority`, `translations` |
Delete a tax zone
/api/v1/kahunacart/tax/zones/{id}
Delete a tax zone and its rates. Orders already placed keep the tax they were charged, because an order line carries its own snapshot.
Parameters
| Name | Type | Description |
|---|---|---|
| id required | integer | Tax zone id |
Import EU VAT presets
/api/v1/kahunacart/tax/presets/eu-vat
Create a tax zone and rate for each EU member state at its standard VAT rate, and report how many were actually added. Idempotent by zone name, so calling it twice does not duplicate a jurisdiction and the count is what was added rather than what was offered.
List tax drivers
/api/v1/kahunacart/tax/engines
Every tax engine an add-on registered, the store-level default engine code, whether the built-in zone and rate editors still govern anything, and the registered tax-id validators. KahunaCart ships no driver of its own, so engines is empty on a stock install and the connectors are add-ons; administration.state is full, partial or none, and none means the zone, class and rate routes answer 409.
Bind a tax driver to a region
/api/v1/kahunacart/tax/engines/bind
Attach a registered tax engine to one tax zone, so a store can quote a connector for one jurisdiction and its own rates for another from the same install. Send an empty engine to unbind. The store-level default every unbound zone falls back to is the tax.engine setting rather than anything this tool writes.
Parameters
| Name | Type | Description |
|---|---|---|
| zone_id required | integer | The tax zone to bind |
| engine optional | string | A registered engine code, or an empty string to unbind |
List price preferences
/api/v1/kahunacart/tax/price-preferences
Whether prices contain tax, per region and per currency, plus the published resolution order and the store-wide tax.included at the bottom of it. A store selling to the UK and the US needs both answers at once, which one boolean cannot express.
Create a price preference
/api/v1/kahunacart/tax/price-preferences
Say that one region or one currency is priced inclusive or exclusive of tax. attribute is region (the value is a tax zone id) or currency (the value is an ISO 4217 code). One answer per region per currency; creating a second for the same key is refused with 409.
Parameters
| Name | Type | Description |
|---|---|---|
| attribute required | string |
What the preference is written against
One of:
region, currency |
| value required | string | A tax zone id for region, an ISO 4217 code for currency |
| is_tax_inclusive required | boolean | Whether prices in that scope contain tax |
| currency optional | string | The currency the row is for. Empty, which is the default for a currency preference and for a region preference on a store that sells in more than one currency, means every currency. A currency preference ("prices in EUR contain tax") matches on its value whatever this says. A region preference on a store with one currency defaults to the store currency, as it always did. |
Update a price preference
/api/v1/kahunacart/tax/price-preferences/{id}
Change a preference's scope or its answer. Omitted fields are left standing.
Parameters
| Name | Type | Description |
|---|---|---|
| id required | integer | Price preference id |
| attribute optional | string |
One of:
region, currency |
| value optional | string | |
| is_tax_inclusive optional | boolean | |
| currency optional | string | The currency the row is for; empty is every currency. See create_price_preference |
Delete a price preference
/api/v1/kahunacart/tax/price-preferences/{id}
Remove a preference. The scope it covered falls back to the next level of the resolution order — the currency preference, then tax.included.
Parameters
| Name | Type | Description |
|---|---|---|
| id required | integer | Price preference id |
Import the tax rate table
/api/v1/kahunacart/tax/rates/import
Import the 10-column rate table — country_code, state_code, postcode, city, rate, tax_rate_name, priority, compound, shipping, tax_class — as a csv string. Rows sharing a geography become one zone carrying several rates, which is what makes GST plus QST expressible. Idempotent on country, state, postcode, city, tax class and priority, so importing the same file twice updates rather than duplicates. A malformed row fails alone and comes back in errors with its line number.
Parameters
| Name | Type | Description |
|---|---|---|
| csv required | string | The rate table as CSV text, header row optional |
Export the tax rates as CSV
/api/v1/kahunacart/tax/rates/export
The whole rate table as a text/csv file. No MCP tool: the answer is a document to edit, not a payload to read
Zone fields are name, countries, regions, postcodes, cities, priority and engine. A rate takes rate (a decimal string, required), label, tax_class, apply_to_shipping and priority. The preset import is idempotent by zone name, so imported is what was actually added.
The rate CSV is Woo's ten columns, because a merchant arriving from Woo has one of these files already. Import is idempotent on the geography, the class and the priority, so running the same file twice updates rather than doubles. A malformed row fails alone with its line number and every other row still imports. Rows sharing a geography fold into one zone carrying several rates, which is what makes a stacked GST and QST expressible at all, so write the most specific geography first. A round trip changes nothing: export, import the file back, export again, and the two files match byte for byte.
A price preference is an attribute of region or currency, the value it names, and is_tax_inclusive. Nothing else is accepted as an attribute, and a unique key over (attribute, value, currency) makes two rows that disagree impossible. Preferences sit in the middle of a published four-level resolution order: the product's own price_includes_tax first, then the region preference, then the currency preference, then the store-wide tax.included. The level that answered is recorded on the adjustment, so a receipt six months old can still say why it was inclusive.
A tax engine quotes tax for a region through its own service instead of the rate table. Binding is per zone rather than per store, so one install can quote a connector for the United States and its own EU VAT rates for Europe, and an empty engine on the bind route unbinds. KahunaCart ships the contract and no driver, so GET /kahunacart/tax/engines is empty on a stock install and lists whatever the tax add-on registered otherwise. A zone bound to an engine nobody registered falls back to the merchant's own rates rather than taking checkout down, and the read says which zones are in that state. When a bound engine has taken the built-in rate administration away, the four zone writes, the EU VAT preset and the CSV import all answer 409 naming the engine. The bind route is left out of that on purpose: it is the route that undoes a binding, and a store has to be able to unlock its own rate table.