Layered & additive stacking

This is the single most important idea in TaxLens. Every accommodation tax is modeled as its own single-jurisdiction layer, every matching layer fires independently, and their amounts sum. There is no stored 'combined rate' anywhere — and that is by design.

The principle

A stay in a real city is taxed by several authorities at once. In Fort Worth, Texas the state levies a hotel occupancy tax, the city levies its own, and a convention-center district levies a third. Each authority sets its rate independently and remits to itself. TaxLens mirrors that reality exactly: each authority's tax is a separate row, and the engine fires every row that matches and adds the results.

This is the industry-standard layered tax model. It keeps each authority's liability attributable on its own line, which is what per-jurisdiction remittance reporting and invoice reconciliation both depend on.

Additive stacking — a Fort Worth stay
Room (base)$1,000Texas state HOT 6% · US-TX+ $60.00Fort Worth city HOT 9% · US-TX-FTW+ $90.00Convention-center district 2% · US-TX-FTW+ $20.00Total tax (17%)$170.00
Three authorities, three layers, one sum. Each is its own row at its own node.

Author every layer as its own rate

The corollary is a hard authoring rule: never store a combined total. The Texas 6% and the Fort Worth 9% are two rows, at two nodes — not a single "15%" parked on the city. The same goes everywhere:

  • US state lodging tax + city occupancy tax → two rows.
  • Canadian provincial sales tax + municipal accommodation tax → two rows.
  • Japan prefecture tax + village tax → two rows.
  • Barcelona's regional Catalan IEET + municipal recargo → two rows.

Why it matters beyond tidiness: when one authority changes its rate — a state raises HOT by a point — exactly one row updates, and every city beneath it reflects the change automatically. A combined-total scheme would force a manual edit to every descendant and quietly drift out of date.

The combined-total trap
If a city stored 15% and the state's 6% still fired from its own row, the engine would stack 6% + 15% = 21% — silently overcharging. Combined totals are double-counting waiting to happen. One layer, one row.

See the layers fire

Resolve the raw layers in force on a chain, then run a real calculation and watch each one appear as its own component in tax_breakdown.components, tagged with the node it came from.

GET/v1/jurisdictions/US-TX-FTW/effective-rates
Sign in to run

The active layers on the Fort Worth chain — each row is its own standalone layer at its own node (the live data may carry a city layer as a single combined row rather than the split shown in the diagram above).

Sign in to run this against the live API. Read-only — nothing is saved.

POST/v1/tax/calculate
Sign in to run

One calculation — every matching layer fires independently and the components carry their own jurisdiction_code.

Request body
{
  "jurisdiction_code": "US-TX-FTW",
  "stay_date": "2026-07-01",
  "nights": 2,
  "nightly_rate": 500,
  "currency": "USD",
  "property_type": "hotel"
}

Sign in to run this against the live API. Read-only — nothing is saved.

There is no aggregate rate field you have to trust blindly: the total is just the sum of the components, and each component names its authority. See the request shape in detail in Tax categories and the full response in How a calculation works.

Replacement, expressed as exemption rules

Sometimes the law isn't additive — a special zone replaces the national rate rather than adding to it. TaxLens does not model this with a special "replacement rate". The engine is structurally additive; replacement is expressed as an exemption rule attached to the ancestor rate, scoped by jurisdiction code.

Take Tierra del Fuego (AR-TF), a special customs and tax regime in Argentina. Rather than authoring a 0% VAT rate at AR-TF, we leave Argentina's national IVA rate intact and hang an exemption rule on it that fires only for descendants of AR-TF:

{
  "rule_type": "exemption",
  "conditions": {
    "operator": "AND",
    "rules": [
      { "field": "jurisdiction_code", "op": "starts_with", "value": "AR-TF" }
    ]
  },
  "action": { "type": "exempt" },
  "legal_reference": "Ley 19.640 — Special Customs and Tax Regime for Tierra del Fuego"
}

For an AR-TF stay the rule fires, the national VAT is waived, and the response still shows that VAT component — flagged as exempted by the rule, with its legal basis attached. For any other Argentine stay the rule's condition fails and VAT applies normally. The same pattern handles the EU VAT cascade: France's mainland VAT carries an exemption scoped to French Guiana (FR-GF, the DOM-TOM code the rule matches — the same rule also covers the other overseas codes FR-GP / FR-MQ / FR-RE / FR-YT and the Pacific COM codes), and Greece's national accommodation VAT carries one scoped to the qualifying small Aegean islands, each paired with the local reduced rate that fires in its place.

Replacement = ancestor rate + exemption rule
AR national VAT 21%stays on file at ARexemption rule: starts_with "AR-TF"AR-TF stay → VAT waived (0%)component shown, flagged "exempted"other AR stay → 21% firesrule condition fails, no change
The national rate stays on file. An exemption rule scoped to the zone waives it there; the local rate fires instead.
Detail
This is more transparent than a replacement rate: the legal basis lives in the rule, one rule covers a whole zone (no per-city duplication), and it composes uniformly with every other booking-conditional exemption (long-stay, diplomatic). Read the full rule model in Rules.

Where to go next

Layers can be different kinds of tax that compute differently — see Tax categories. Some layers even fold another layer into their base — see VAT base composition. And rules decide which layers fire for a given booking — see Rules.