Appearance
Tax Rates
Tax rates are defined once on the location and referenced by id from products, modifiers and menu items. This guide covers the TaxRate object, how the rate value is encoded, how to set rates through each endpoint, and how the references flow through to orders.
Overview
A location owns a list of tax rates. Every catalog and menu entity that is taxed points at one or more of those rates through a taxRateIds array. The two halves are joined by the tax rate's posReference.
The location's taxRates array is the source of truth; everything else holds a reference to it.
Key principles:
- The location is the single source of truth. A
taxRateIdsvalue that does not match aposReferenceon the location refers to nothing. posReferenceis the primary identifier. It is the value you configure against products and modifiers — notname, and not any POS Hub internal id.- POS Hub does not calculate tax. It stores tax rates and relays them, along with the
taxRateIdsreferences, to connected applications. The consuming application — your POS, or a marketplace — performs the calculation. Fields such asinclusiveandisDefaultare carried through for those consumers rather than interpreted by POS Hub. - Menu items inherit from the catalog. When a menu is built from catalog products,
taxRateIdsis copied onto the menu item; you do not set it separately unless you want a menu-specific override.
The TaxRate object
| Field | Type | Required | Notes |
|---|---|---|---|
posReference | string | no | The primary identifier of the rate, and the value products and modifiers reference. Send your POS system's tax identifier. If omitted, POS Hub generates a UUID. |
name | string | yes | Human-readable, as the merchant knows it — "NY Sales Tax", "VAT 20%", "Service Fee". Minimum length 1. |
type | enum | yes | FIXED or PERCENTAGE. Determines how rate is read. |
rate | integer | yes | Scaled integer — see Encoding the rate. Range 0–1000000. |
inclusive | boolean | no | true if the tax is already included in listed prices (typical for VAT), false if it is added at checkout. Defaults to false. |
isDefault | boolean | no | Marks this as the location's default rate. Defaults to false. |
Encoding the rate
rate is always an integer, and how it is interpreted depends on type. This is the single most common source of error — getting it wrong is a three-orders-of-magnitude mistake, not a rounding one.
type | Meaning | Example |
|---|---|---|
PERCENTAGE | The percentage multiplied by 10,000 | 20% → 2000006.25% → 6250012.275% → 122750 |
FIXED | A flat amount in the smallest currency unit | $1.50 → 150£0.30 → 30 |
The maximum accepted value, 1000000, is therefore exactly 100% for a percentage rate.
If your POS expresses percentages as decimals (6.25% as 0.0625), multiply by 1,000,000. If it expresses them as whole percents (6.25), multiply by 10,000.
Defining tax rates on a location
Tax rates live in the location's taxRates array. There are three ways to set them.
On location create
POST /v1/accounts/{accountId}/locations
json
{
"name": "Downtown Restaurant",
"taxRates": [
{
"posReference": "vat-20",
"name": "VAT 20%",
"type": "PERCENTAGE",
"rate": 200000,
"inclusive": true,
"isDefault": true
},
{
"posReference": "service-fee",
"name": "Service Fee",
"type": "FIXED",
"rate": 150,
"inclusive": false
}
]
}On location patch
PATCH /v1/accounts/{accountId}/locations/{locationId}
json
{
"taxRates": [
{
"posReference": "vat-20",
"name": "VAT 20%",
"type": "PERCENTAGE",
"rate": 200000,
"inclusive": true,
"isDefault": true
}
]
}Through a catalog import
PUT /v1/accounts/{accountId}/locations/{locationId}/catalog/import
The import accepts a location block, so rates and the products that reference them can be sent in a single request. See Worked example.
Update semantics
taxRates behaves as a whole-array replacement, not a merge. Three cases:
| You send | Result |
|---|---|
taxRates omitted from the request | Existing rates are left untouched. A partial update — an opening-hours sync, for example — will not disturb them. |
taxRates: [ … ] | The location's rates are replaced entirely by what you sent. A rate you leave out is removed. |
taxRates: [] | All rates are cleared. |
Because the array is replaced rather than merged, always send the complete set of rates for the location, not just the ones that changed.
Generated posReference
posReference is optional. When you omit it, POS Hub generates a UUID and returns it on the response:
json
// Request
{ "taxRates": [{ "name": "VAT 20%", "type": "PERCENTAGE", "rate": 200000 }] }
// Response
{
"data": {
"taxRates": [
{
"posReference": "3f2a91b4-7c0e-4d8a-9b21-5e6f0c8d44a1",
"name": "VAT 20%",
"type": "PERCENTAGE",
"rate": 200000
}
]
}
}A posReference you supply is always preserved exactly — POS Hub never overwrites it.
Send your own posReference where you can
Because the array is replaced wholesale on every write, a rate created with a generated posReference gets a new id if you later send the array again without it — and every taxRateIds pointing at the old value silently stops resolving. Send a stable identifier from your POS system, and reuse it on every update.
Referencing rates from products and modifiers
Set taxRateIds to the posReference values of the rates that apply. An entity can reference several rates.
json
{
"posReference": "burger-001",
"name": "Cheeseburger",
"price": 899,
"taxRateIds": ["vat-20", "service-fee"]
}This field is available on:
| Entity | Endpoints |
|---|---|
| Catalog product | create, patch, catalog import |
| Catalog modifier | create, patch, catalog import |
| Menu item | create, patch (normally inherited from the catalog product) |
| Menu modifier | create, patch (normally inherited from the catalog modifier) |
Clearing references
taxRateIds is always an array of strings:
| You send | Result |
|---|---|
taxRateIds: ["vat-20"] | Those rates apply. |
taxRateIds: [] | Previously set values are cleared. |
taxRateIds omitted | Previously set values are cleared. |
Note the last row: on import, omitting the field is not the same as leaving it alone. An import is a full statement of the catalog, so a product sent without taxRateIds ends up with none. If you want a product to keep its rates, send them on every import.
What happens at order time
When an order arrives, POS Hub resolves taxRateIds onto each order line from the catalog entity the line maps to, so the rates that reach your POS reflect the catalog as configured — not whatever the marketplace happened to send:
json
{
"items": [
{
"name": "Cheeseburger",
"quantity": 1,
"price": 899,
"taxRateIds": ["vat-20"],
"options": [
{ "name": "Extra Cheese", "price": 100, "taxRateIds": ["vat-20"] }
]
}
]
}Resolution falls back in this order: the catalog product or modifier the line maps to, then any taxRateIds on the incoming payload, then the catalog map. A line whose product has no rates configured carries an empty array.
Worked example
A single catalog import that defines the location's rates and wires products and modifiers to them:
json
{
"location": {
"taxRates": [
{
"posReference": "vat-20",
"name": "VAT 20%",
"type": "PERCENTAGE",
"rate": 200000,
"inclusive": true,
"isDefault": true
},
{
"posReference": "vat-05",
"name": "Reduced VAT 5%",
"type": "PERCENTAGE",
"rate": 50000,
"inclusive": true
}
]
},
"categories": [{ "posReference": "cat-food", "name": "Food" }],
"products": [
{
"posReference": "burger-001",
"name": "Cheeseburger",
"price": 899,
"categories": ["cat-food"],
"taxRateIds": ["vat-20"]
},
{
"posReference": "coffee-001",
"name": "Takeaway Coffee",
"price": 320,
"categories": ["cat-food"],
"taxRateIds": ["vat-05"]
}
],
"modifiers": [
{
"posReference": "mod-cheese",
"name": "Extra Cheese",
"price": 100,
"taxRateIds": ["vat-20"]
}
],
"modifierGroups": []
}Common mistakes
| Symptom | Cause |
|---|---|
| Tax comes out 100× or 10,000× wrong | rate sent as a plain percentage (20) or a decimal (0.2) instead of percent × 10,000 (200000). |
taxRateIds resolves to nothing | The values are not posReference values — commonly a rate's name, or a POS Hub internal id. |
| Rates disappear after an update | taxRates was sent as a partial list. The array is replaced, not merged. |
| References break after a location update | The rate was created without a posReference, so a new one was generated on the next write. Send a stable id from your POS. |
| Products lose their rates after an import | taxRateIds was omitted. On import, omitted means cleared. |
Related
- Synchronization Processes — how location data, including tax rates, is kept in sync
- Menu Structure — how catalog products become menu items
- Integration Flow — where the catalog import sits in the wider POS flow
