Quote a stay
The quote request and every field of the answer, from lines, nights and evidence to unknowns, totals and assumptions, in integer minor units.
quote(release, request) prices one stay against a loaded release and returns a Quote. It is a pure function: same release, same request, same answer.
import { quote, TaxDataError } from "./calculator";
const answer = quote(release, {
place: { jurisdiction: "de.berlin" },
stay: { arrival: "2026-11-12", departure: "2026-11-14" },
rooms: [
{
guests: [{ age: 41 }, { age: 38 }],
price: { currency: "EUR", vat: "included", totalMinor: 32000 },
},
],
});All money, in the request and the answer, is in integer minor units of its currency: 32000 is €320.00. The manifest's currencies gives each currency's exponent.
The request
Top level
| Field | Type | Meaning |
|---|---|---|
place | object | Where the stay is. See Resolve a place for both forms. |
stay.arrival | YYYY-MM-DD | The arrival date. |
stay.departure | YYYY-MM-DD | The departure date. The stay is the nights from arrival up to, not including, departure: 1 to 366 nights. |
rooms | array | 1 to 50 rooms, each priced on its own. |
vatBps | integer, optional | The accommodation VAT rate in basis points (1000 is 10%), for every night. Only charges levied on the room price use it. Without it the calculator uses the rate the release has in force for the place on each night, and a night with none makes such a charge unknown (vat_unknown). |
release | release id, optional | Not read by the calculator, which prices with the release you pass it. |
A room
| Field | Type | Meaning |
|---|---|---|
guests | array | 1 to 30 guests. |
guests[].age | integer 0–130, optional | The guest's age on arrival. A guest with no age is taken as liable at the full rate, and the answer says so in assumptions. |
guests[].groups | string array, optional | Exemption groups the guest belongs to, as the law's exemptions name them, such as medical_treatment or seasonal_worker. A line's claimable lists the groups that would lower it. |
price | object, optional | The room's price. Only charges levied on the price need it. |
A room price
| Field | Type | Meaning |
|---|---|---|
currency | ISO 4217 code | The price's currency. The calculator never converts: a price-based charge in another currency is unknown. |
vat | "included" or "excluded" | Whether the amounts include accommodation VAT. |
nights | array | One entry per night of the stay: date, amountMinor and, optionally, breakfastMinor inside it. Give this or totalMinor, not both. |
totalMinor | integer | The stay's price, spread evenly over its nights. |
breakfastTotalMinor | integer, optional | The breakfast inside totalMinor. Only with totalMinor. |
breakfastVatBps | integer, optional | The VAT rate on breakfast, needed when a charge's base includes breakfast and the price states VAT the other way from the base. |
The answer
Top level
| Field | Meaning |
|---|---|
release | The id of the release the quote was priced with. |
calculator | The calculator's version, such as 1.0.0. |
status | complete, price_required or partial. See Status. |
place | The place as the calculator located it. See Resolve a place. |
lines | One line per room, charge and law version that applies. See Lines. |
priceDependent | Charges that need a room price the request didn't give. See Price-dependent charges. |
unknowns | What the release can't price for this stay. See Coverage and unknowns. |
totals | Per currency, the sum of the lines. See Totals. |
assumptions | What the calculator took as given. See Assumptions. |
meta | The release's terms: its id, the licence id and URL, the attribution notices and the disclaimer. Show the attribution and the disclaimer wherever you show the taxes. |
Status
| Status | Meaning |
|---|---|
complete | Every charge that applies was priced, and unknowns and priceDependent are empty. |
price_required | Nothing is unknown, but at least one charge is levied on the room price and the request gave none. priceDependent lists them. Quote again with a price. |
partial | At least one thing is unknown. The lines and totals cover only what could be priced; unknowns says what is missing. |
partial wins over price_required: a quote with an unknown is partial even when it also needs a price.
Lines
A line is one charge, under one law version, for one room. A stay that crosses the date a new version comes into force gets one line per version. Lines are ordered by room, then from the country down to the most local jurisdiction, then by charge id.
Line fields
| Field | Meaning |
|---|---|
room | The room's index in the request, from 0. |
charge | The charge id, such as es.catalunya.tourist-tax. Its first two letters are the country. |
version | The law version: the date this version of the charge came into force. |
surchargeOf | Only on a surcharge levied as a share of other charges: the charges it is a share of. |
kind | What sort of charge it is, such as city_tax. |
name, nameEn | The charge's name in the law's language and in English. |
jurisdiction | The jurisdiction that levies it. |
currency | The line's currency. |
amountMinor | The charge for the room over the line's nights, rounded as the law rounds it. |
vatMinor | VAT on the charge itself, where the law puts VAT on it. Usually 0. |
inPrice | true when the law counts the charge as part of the room price, false when it is added on top. |
payer | guest or operator: who the law makes owe it. |
collectedBy | Who collects it: operator, platform_when_paid (the platform when the guest pays through it) or platform_always. |
nights | Each night the line applies to: date, liable (the guests counted that night), amountMinor and vatMinor. |
explanation | A summary of the line. See Explanation. |
claimable | Exemptions a guest could claim but the request didn't. See Claimable exemptions. |
evidence | The ids of the sources behind the version, its rates and its exemptions. |
The nights add up to the line: amountMinor is the sum of the nights' amountMinor, and the same for VAT.
Explanation
explanation.code names the way the charge is levied: per_person_night, per_room_night, per_person_stay, per_room_stay, percent, percent_capped, banded, surcharge, or mixed when the line's nights fall under different rates. explanation.params holds the numbers behind it:
nights: how many nights the line covers;liable: the most guests liable on any one night;amount:amountMinorwritten out with its currency, such as20.40 EUR;exempt: present only when an exemption lowered the line, the ids of every exemption that did, comma-separated and sorted.
explanation.text puts the same in an English sentence, for logs and support screens. Build your own wording from code and params.
Claimable exemptions
Some exemptions depend on who the guest is, not only their age: a group the law names, such as medical_treatment. When no guest in the request claims such a group, the line lists it in claimable with its exemption id and group. When a guest claims it but the exemption also has an age range and the guest has no age, the entry carries needs: "age". Ask the guest, then quote again with groups (and the age) on that guest.
Evidence
Each id in evidence is an entry in the country's evidence list, in the same release:
| Field | Meaning |
|---|---|
id | ev_ and 16 hex characters. |
url | Where the source is published. Always https. |
quote | The source's own words, verbatim, at most 500 characters. |
english | An English translation of the quote, when the source isn't in English. |
publisher | Who publishes the source. |
checkedOn | The date the quote was checked against the source. |
The quickstart shows how to look them up.
Price-dependent charges
A percentage of the room price, or a charge banded by price, can't be priced without one. When the request gives no price for a room, each such charge is listed once per room instead of as a line:
{
"room": 0,
"charge": "de.berlin.city-tax",
"version": "2025-01-01",
"nights": ["2026-11-12", "2026-11-13"]
}A surcharge on a price-dependent charge is price-dependent too.
Totals
totals has one entry per currency, ordered by currency:
| Field | Meaning |
|---|---|
currency | The currency. |
addedMinor | The lines added on top of the room price: amountMinor plus vatMinor, summed. |
inPriceMinor | The lines already inside the room price, summed the same way. |
The Berlin request above comes back complete, with one line of 2243 and VAT on the charge of 157:
{ "currency": "EUR", "addedMinor": 2400, "inPriceMinor": 0 }The calculator never adds across currencies.
Assumptions
Each assumption has a code and an English text:
| Code | When |
|---|---|
age_missing_taken_as_liable | A guest has no age, and an age band could have lowered what they owe. They are charged in full. |
night_cap_per_booking | A cap on nights per calendar year at the property applies. The calculator counts this booking's nights only. |
long_stay_per_booking | A long-stay limit applies. The calculator counts this booking's nights only, not an earlier or later booking at the same property. |
Errors
A request the calculator can't take throws a TaxDataError, with a code and, when one field is at fault, a pointer to it (an RFC 6901 JSON pointer such as /place/jurisdiction).
try {
quote(release, request);
} catch (error) {
if (error instanceof TaxDataError) console.log(error.code, error.pointer);
else throw error;
}Error codes
| Code | Meaning |
|---|---|
request_invalid | The request doesn't match the schema. |
stay_invalid | Departure is not after arrival. |
quote_too_large | More than 366 nights, 50 rooms or 30 guests in a room. |
price_invalid | The price doesn't fit the stay: a night missing or given twice, breakfast above the price, or in-price charges above the price. |
breakfast_vat_required | A charge's base counts breakfast with VAT the other way from the price, and the request gives no breakfastVatBps to convert it. |
country_unknown | The place's country isn't in the loaded release. |
jurisdiction_unknown | No such jurisdiction in the country. |
lau_year_mismatch | The LAU code's year isn't the country's LAU year. |
place_within_mismatch | within is in another country, isn't in the place's chain, or, for a LAU code the release doesn't list, isn't a country or region. |
type_not_in_classification | The type isn't one the place's classification has. |
class_not_in_classification | The class isn't one the place's classification has for that type. |
Quickstart
Load a signed Octroi release, quote three nights at a 4-star hotel in Barcelona for two adults and a child, and read the lines, totals and evidence.
Resolve a place
Name a place by jurisdiction id or LAU code, and get the charges in force there on a date, with their rates, exemptions and evidence.