Skip to content
Octroi

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

FieldTypeMeaning
placeobjectWhere the stay is. See Resolve a place for both forms.
stay.arrivalYYYY-MM-DDThe arrival date.
stay.departureYYYY-MM-DDThe departure date. The stay is the nights from arrival up to, not including, departure: 1 to 366 nights.
roomsarray1 to 50 rooms, each priced on its own.
vatBpsinteger, optionalThe 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).
releaserelease id, optionalNot read by the calculator, which prices with the release you pass it.

A room

FieldTypeMeaning
guestsarray1 to 30 guests.
guests[].ageinteger 0–130, optionalThe 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[].groupsstring array, optionalExemption 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.
priceobject, optionalThe room's price. Only charges levied on the price need it.

A room price

FieldTypeMeaning
currencyISO 4217 codeThe 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.
nightsarrayOne entry per night of the stay: date, amountMinor and, optionally, breakfastMinor inside it. Give this or totalMinor, not both.
totalMinorintegerThe stay's price, spread evenly over its nights.
breakfastTotalMinorinteger, optionalThe breakfast inside totalMinor. Only with totalMinor.
breakfastVatBpsinteger, optionalThe 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

FieldMeaning
releaseThe id of the release the quote was priced with.
calculatorThe calculator's version, such as 1.0.0.
statuscomplete, price_required or partial. See Status.
placeThe place as the calculator located it. See Resolve a place.
linesOne line per room, charge and law version that applies. See Lines.
priceDependentCharges that need a room price the request didn't give. See Price-dependent charges.
unknownsWhat the release can't price for this stay. See Coverage and unknowns.
totalsPer currency, the sum of the lines. See Totals.
assumptionsWhat the calculator took as given. See Assumptions.
metaThe 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

StatusMeaning
completeEvery charge that applies was priced, and unknowns and priceDependent are empty.
price_requiredNothing 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.
partialAt 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

FieldMeaning
roomThe room's index in the request, from 0.
chargeThe charge id, such as es.catalunya.tourist-tax. Its first two letters are the country.
versionThe law version: the date this version of the charge came into force.
surchargeOfOnly on a surcharge levied as a share of other charges: the charges it is a share of.
kindWhat sort of charge it is, such as city_tax.
name, nameEnThe charge's name in the law's language and in English.
jurisdictionThe jurisdiction that levies it.
currencyThe line's currency.
amountMinorThe charge for the room over the line's nights, rounded as the law rounds it.
vatMinorVAT on the charge itself, where the law puts VAT on it. Usually 0.
inPricetrue when the law counts the charge as part of the room price, false when it is added on top.
payerguest or operator: who the law makes owe it.
collectedByWho collects it: operator, platform_when_paid (the platform when the guest pays through it) or platform_always.
nightsEach night the line applies to: date, liable (the guests counted that night), amountMinor and vatMinor.
explanationA summary of the line. See Explanation.
claimableExemptions a guest could claim but the request didn't. See Claimable exemptions.
evidenceThe 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: amountMinor written out with its currency, such as 20.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:

FieldMeaning
idev_ and 16 hex characters.
urlWhere the source is published. Always https.
quoteThe source's own words, verbatim, at most 500 characters.
englishAn English translation of the quote, when the source isn't in English.
publisherWho publishes the source.
checkedOnThe 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:

FieldMeaning
currencyThe currency.
addedMinorThe lines added on top of the room price: amountMinor plus vatMinor, summed.
inPriceMinorThe 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:

CodeWhen
age_missing_taken_as_liableA guest has no age, and an age band could have lowered what they owe. They are charged in full.
night_cap_per_bookingA cap on nights per calendar year at the property applies. The calculator counts this booking's nights only.
long_stay_per_bookingA 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

CodeMeaning
request_invalidThe request doesn't match the schema.
stay_invalidDeparture is not after arrival.
quote_too_largeMore than 366 nights, 50 rooms or 30 guests in a room.
price_invalidThe 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_requiredA charge's base counts breakfast with VAT the other way from the price, and the request gives no breakfastVatBps to convert it.
country_unknownThe place's country isn't in the loaded release.
jurisdiction_unknownNo such jurisdiction in the country.
lau_year_mismatchThe LAU code's year isn't the country's LAU year.
place_within_mismatchwithin 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_classificationThe type isn't one the place's classification has.
class_not_in_classificationThe class isn't one the place's classification has for that type.

On this page