The sources we read
Every rate starts from a published text: a national gazette, a regional or municipal ordinance, a council resolution, a tax authority's circular, or a municipality's own page where that is how it publishes. The research reads the text that sets the charge, in its own language, rather than a summary of it.
Each source is named as it names itself. A row from Rome cites Roma Capitale, one from Croatia Narodne novine, one from Paris the Ville de Paris. The library refuses evidence from Google, an online travel agency or any other seller of rooms: a price page is not a law.
The words, kept verbatim
A rate never stands on its own. Every charge version, rate, exemption, VAT rate and property type cites at least one piece of evidence, and a country file that cites evidence it doesn't hold, or holds evidence nothing cites, is refused. Each piece of evidence holds:
- the passage that sets the rule, quoted word for word in the source's language;
- an English translation of it, where the passage isn't in English and one has been written;
- the publisher, as the source names itself;
- the URL, always https;
- the day it was checked.
Its id is derived from the URL and the quote: the first 16 hex characters of a sha256 of both. A changed quote is new evidence with a new id, never a silent edit to an old one.
Checked, and watched
Each piece of evidence carries the day it was last checked, and each country the day its research was last confirmed. A quote's lines cite their evidence, so the date travels with every number.
A watch refetches every source a release cites. It keeps a hash of each page's text and whether the quote is still on it. A source whose quote has gone missing, whose page has changed, or that has stayed unreachable for three days is flagged, with the places it supports, for a person to recheck. A first reading has no earlier hash to compare, so it can flag a missing quote but not a changed page, and the watch never marks anything resolved by itself.
How a place is identified
Charges are levied by jurisdictions: a country, its regions, an association of municipalities, a municipality, a zone inside it. Each carries the codes it is known by. A municipality carries its LAU code from Eurostat's list for the country's LAU year, regions carry NUTS codes where they have them, and a quote can be asked for by LAU code alone.
Eurostat's units don't always match the level that levies a charge. In Greece and Portugal a municipality can hold several LAU codes, because the LAUs there are communities and parishes below it. A city that is its own region, like Vienna, Zagreb or Berlin, is a region holding the municipality.
A rate can name an area inside its jurisdiction. Once a version names one, only the rates naming that area apply inside it, and the rest apply only outside every named area: nothing falls back from an area to the town around it.
Attribution
LAU and NUTS codes: © European Union, Eurostat, LAU 2024 and NUTS 2024, reused under Eurostat's general reuse notice (https://ec.europa.eu/eurostat/about-us/policies/copyright).
How a release is built and signed
A release is built from the researched law, and refused whole if anything in it is wrong. The build reports every problem at once and writes nothing if it finds one:
- a country file that breaks a rule of the schema;
- a property type, class, area and day with no rate, or with two;
- a hand-worked test vector the calculator disagrees with, or a rate no vector reaches;
- more unpriced charge versions than the last release, without a listed reason for each new one.
A release is named by a date, each later than the last. Once published it is immutable: nothing updates or deletes a release or its files. Its manifest names every file with its path, sha256 and size, the licence text included, and the manifest is signed with Ed25519. Every quote names the release and the calculator version that made it.
Verifying a release
- Fetch the public key from
GET /tax-data/v1/signing-key, an SPKI PEM. It needs no token. - Check
manifest.json.sig, a raw 64-byte Ed25519 signature, against the bytes ofmanifest.json. - Check each file you read against the manifest's sha256 and size.
A file that passes is byte for byte the one that was signed, licence and disclaimer included.
What the calculator does with a stay
Given a place, the arrival and departure, the rooms and their guests, and each room's price if you have it, the calculator works night by night:
- It finds the charges levied at each level of the place, and the version of each in force that night.
- It picks the rate for the property's type and class, the named area and the season the night falls in.
- It applies exemptions: age bands and guest groups, each guest taking the lowest factor they qualify for; night caps, per stay or per calendar year at the property; and long-stay limits, which take a whole stay out of a charge.
- It prices a percentage or banded charge from the room price, taking VAT and breakfast out of the base, or leaving them in, as the law's base says, and caps it where the law caps it.
- It adds surcharges on the charges they are levied on, and VAT on a charge where the law levies it.
Amounts stay exact fractions until they are rounded, once a line unless the law rounds each night or each guest-night, half up unless the law says otherwise. A line is then spread over its nights so they add up exactly. The totals keep what is added to the bill apart from what is already inside the price, and the calculator never converts a currency.
A group exemption no guest claimed is listed as claimable, not applied, so you can ask the guest. Where the calculator has to assume something, the quote says what: a guest with no age is taken as liable at the full rate, and a night cap or long-stay limit counts this booking's nights only.
When a quote says it doesn't know
A quote has one of three statuses. It is complete when every charge is priced. It is price_required when a charge depends on a room price the request didn't give; the nights that need one are listed. It is partial when something couldn't be priced at all, and each gap is an unknown with its reason:
jurisdiction_not_researched: a level of the place nobody has researched yet;type_unknown,class_unknown: the rate depends on a property type or class the place doesn't give;zone_unresolved: the rate depends on a zone inside the place, which it doesn't name;unpriced_rule: the law sets the charge in a way the data can't price;currency_mismatch: a price-based charge in another currency than the room's price;vat_unknown: a base that needs the VAT rate when none is in force and the request gives none.
Some rules can't be priced from the law alone. Bologna's 2026 rate depends on the channel a room is sold through, for one. Such a charge version is written as unpriced, with its reason, and each country's unpriced versions are listed with theirs. A release that adds one without listing it is refused.