Rules: exemptions, overrides, caps, surcharges, reductions
A raw rate is rarely the whole story. Rules sit on top of rates and adjust them based on the specifics of a booking — a long stay goes exempt, a flat tax caps after N nights, a class of property gets a reduced rate. Five effects, a strict condition language, and a full audit trace on every calculation.
What a rule is
A rule is a conditional modifier on a rate. It has conditions (does this booking qualify?) and an action (what to do if it does). The engine evaluates the rules attached to each rate, applies the ones whose conditions match, and records every rule it considered — so the final number is never a black box.
A rule attaches in one of two ways:
- To a specific rate, via
tax_rate_id— it modifies that one layer. - To a jurisdiction, applying to every rate at that node — or, with
target_jurisdiction_codes, fanning out to rates at a set of descendant nodes (covered below).
The five effects
Each rule's action is one of five kinds. They compose in a defined order:
| Field | Type | Description |
|---|---|---|
| exemption | short-circuit | When conditions match, the rate doesn't apply. Exemptions short-circuit — once a layer is exempted, nothing else runs on it. Used for long-stay relief, special zones (AR-TF, Guiana), diplomatic exemptions. |
| override | replace rate_value | Replaces the rate value entirely — e.g. an override to rate_value: 0 carrying an exemption_reason for a reverse-charged commission line (reverse charge). |
| cap | most restrictive wins | Limits the result — a maximum tax amount (max_amount) or a maximum number of taxable nights (max_nights, e.g. a tourist tax that stops after the 7th night). When several caps apply, the most restrictive wins. |
| surcharge | additive | Adds an additional percentage applied to the taxable base (stacking additively). Surcharges stack additively with one another. |
| reduction | multiplicative | Applies a percentage cut to the rate (reduction_percent). Multiple reductions compose multiplicatively, not additively. |
Conditions reference booking fields
A rule's conditions are a small boolean tree over fields of the booking. The canonical shape is a top-level operator plus a list of {field, op, value} clauses:
{
"rule_type": "exemption",
"conditions": {
"operator": "AND",
"rules": [
{ "field": "nights", "op": ">=", "value": 30 }
]
},
"action": { "type": "exempt" }
}field in a condition must be one of a strict allowlist of booking attributes (nights, nightly rate, guest counts, property type, channel, postal code, and so on). A condition that references anything outside that allowlist resolves to nothing and the rule silently never fires — no error, no tax change. This is why AI-discovered rules are normalized into the canonical shape before they ever go live.A subtle but important detail: the engine reads exactly the canonical {operator, rules:[{field, op, value}]} form and the action keys max_nights / rate_value / reduction_percent / max_amount. Rules authored in other shapes (flat objects, other action key names) are normalized to this form before approval — a shape mismatch is the classic cause of a rule that looks right but quietly does nothing.
Binding and jurisdiction fan-out
Modifier rules (override, cap, surcharge, reduction) must be anchored so they can't fan onto the wrong tax. Either they bind a specific tax_rate_id, or they carry target_jurisdiction_codes to attach to rates at a set of descendant nodes.
The fan-out form is the clean way to express a statewide rule without per-city duplication: a US-Nevada long-stay exemption with target_jurisdiction_codes reaches Las Vegas and Reno without authoring the rule three times. Without one of these anchors, a jurisdiction-wide cap or override would spread onto every category at the node — including VAT — which is exactly the kind of financial-bug-class the engine's write-time guards reject.
tax_rate_id or target_jurisdiction_codes. An unanchored modifier fans onto VAT and produces wildly wrong tax. The platform enforces this at write time and at the database layer.The trace: applied, exempted, skipped
Every calculation returns the rules it evaluated and the outcome of each one. In the response, rules_applied lists each rule with a result bucket:
- applied — the rule's conditions matched and its action changed the outcome.
- exempted — an exemption rule matched and waived a layer.
- skipped — the rule was considered but its conditions did not match.
Run a calculation that triggers a rule and inspect the trace — a 35-night stay where a long-stay exemption may apply:
Watch rules_applied: each rule reports applied, exempted, or skipped, so the result is fully auditable.
{
"jurisdiction_code": "ES-CT-BCN",
"stay_date": "2026-07-01",
"nights": 35,
"nightly_rate": 200,
"currency": "EUR",
"property_type": "hotel",
"number_of_guests": 2
}Sign in to run this against the live API. Read-only — nothing is saved.
Where to go next
See how an exemption rule expresses a whole replacement schedule in Layered & additive stacking, how reverse charge is authored as an override-to-zero in Cross-border B2B & reverse charge, and the full list of fields a condition may reference in Scenario fields.