--- name: vehicle-register description: Compute Estonian motor-vehicle registration fee from plate, VIN or technical data via a public API. module: soidukimaks.mjs execution: get --- # Estonian motor-vehicle registration fee (mootorsõidukimaks, registreerimistasu) Transpordiamet publishes a documented JSON API behind its public tax calculator. It is the **only** unauthenticated, machine-readable route into liiklusregister data — the vehicle background check and the calculator's own web form are both bot-gated, and a person's vehicle data needs ID-login. Read `## Limits`. ## Vetted module (use this first) `soidukimaks.mjs` ships in https://kodanikukratt.ee/kratt-agent.tar.gz (plain ESM, Node 22+) and is re-verified against the live source: ```js import { show } from "./kratt.mjs"; import { registrationFee } from "./soidukimaks.mjs"; const r = await registrationFee({ category: "M1", bodyworkCode: "UNIVERSAAL", seats: 8, co2wltp: 183, technPermMaxLadenMass: 2790, initialRegDate: "2021-01-12", regFeeCalcDate: "2026-01-01", }); show({ kokku: r.totalPrice, baas: r.basePrice, co2: r.co2Price, mass: r.massPrice, vanusekoefitsient: r.ageCoef, valuuta: r.currency, sisend: r.inputs, page: r.page, note: r.note, }); ``` - `registrationFee({...})` passes every parameter in `## Retrieve` straight through, so anything documented there works. It refuses a category the fee does not exist for (only `M1`, `M1G`, `N1`, `N1G`) rather than letting the API answer an unexplained 400 — **and that refusal is not a fee of zero**, it means the vehicle is outside this tax. - **A missing parameter comes back as an error naming what is required**, because the API's own 400 says only `"Invalid data"`. Ask the citizen for the value; never invent a mass, power or CO2 figure. - `inputs` echoes exactly what was sent. Restate those assumptions in the answer — a fee computed from a guessed täismass is a wrong number that looks authoritative. - `note` says which half of the tax this is. It is the **one-off** registreerimistasu, not the annual mootorsõidukimaks, and it applies no exemptions at all. - `registrationFeeByIdentifier({regNr})` / `({vinCode})` is the same shape for one registered vehicle. Use it only with an identifier the citizen supplied about their own car; HTTP 422 means the register has no such vehicle, and nothing else about that vehicle comes back with the fee. ```probe-limit claim HTTP 422 means the register has no such vehicle GET https://apimsm.transpordiamet.ee/v2/msm/regTasu/by-vehicle-identifier?regNr=000XXX expect-status 422 expect /Vehicle not found/ # The control is the technical-parameter endpoint on the same API, which must # still price a vehicle. Without it, an API answering 422 to everything would # read as "no such vehicle" for every plate a citizen ever gives us. control GET https://apimsm.transpordiamet.ee/v2/msm/regTasu/by-technical-parameters?category=M1&bodyworkCode=UNIVERSAAL&co2wltp=183&technPermMaxLadenMass=2790&initialRegDate=2021-01-12&seats=8®FeeCalcDate=2026-01-01 ``` The endpoints below are the fallback if a module call throws. ## Access ```text Base: https://apimsm.transpordiamet.ee/v2/msm/regTasu/ ``` No authentication, no API key, JSON responses. Documented by Transpordiamet in "Mootorsõidukimaksu avaliku kalkulaatori registreerimistasu ja omanikuvahetustasu arvutamise API V2 täiendatud juhend" (PDF, linked from `https://www.transpordiamet.ee/soidukid-ja-maanteeliiklus/soidukid/mootorsoidukimaks`). ## Retrieve Three GET endpoints, all returning the same body: ```text GET /v2/msm/regTasu/by-vehicle-identifier?regNr=287HJG GET /v2/msm/regTasu/by-vehicle-identifier?vinCode=WDF63960313149509 GET /v2/msm/regTasu/by-technical-parameters?category=M1&bodyworkCode=UNIVERSAAL&co2wltp=183&technPermMaxLadenMass=2790&initialRegDate=2021-01-12&seats=8®FeeCalcDate=2026-01-01 ``` Response, all amounts in euros: ```json {"totalPrice":504.96,"co2Price":204.96,"massPrice":0.00,"basePrice":300.00,"ageCoef":0.56} ``` `co2Price` and `massPrice` are already multiplied by `ageCoef`; `ageCoef` is derived from whole years between `initialRegDate` and `regFeeCalcDate`. Parameters for `by-technical-parameters`: - `category` — required; the fee exists only for `M1`, `M1G`, `N1`, `N1G`. - `initialRegDate`, `regFeeCalcDate` — always required, `YYYY-MM-DD`. - `seats` — required for `M1`/`M1G`. - `technPermMaxLadenMass` (täismass), `kerbMass` (tühimass), `maximumNetPower` (kW, combustion engine only) — required unless the vehicle is electric and a CO2 figure is present. - `co2wltp` — required unless electric; `co2nedc` — required when WLTP is missing. With several values, take the smallest non-zero one. - `engineType` — `BENSIIN`, `BENSIIN_HYBRIID`, `ELEKTER`, `DIISEL`, `GAAS`, `BENSIIN_KATALYSAATOR`, `DIISEL_HYBRIID`, `LPG`, `CNG`. - `fuelCombination` — `VESINIKKYTUS`, `KAHEKYTUSELINE`, `MITMEKYTUSELINE`, `YHEKYTUSELINE`, `ELEKTRISOIDUK`, `HYBRIIDSOIDUK`, `SEGAKAHEKYTUSELINE`, `SEGAKYTUSELINE`. - `fuelType` — petrol side `BENSIIN`, `ETANOOL`, `HE-15`; diesel side `DIISEL`, `BIODIISEL`, `XTL`, `CNG_DIISEL`, `LNG_DIISEL`, `LNG_CNG_DIISEL`. - `hybridType` — `NOVC_FCHV`, `NOVC_HEV`, `OVC_HEV`, `OVC_FCHV`; needed when WLTP and NEDC are both missing on a hybrid. - `bodyworkCode` — only matters when the body is `ELAMU`. ## Return Preserve every component (`totalPrice`, `co2Price`, `massPrice`, `basePrice`, `ageCoef`), the identifier or parameter set used, `regFeeCalcDate`, and the exact URL fetched. Say which half of the tax the number is: this is the one-off **registreerimistasu / omanikuvahetustasu**, not the annual tax. ## Limits - **Registration fee only.** The annual tax (aastamaks) is administered by EMTA and has no published public API. Its objects, rates and formulas are in **mootorsõidukimaksu seadus** — fetch the in-force text via `legal-acts-data` and cite the § you printed instead of computing an annual figure yourself. - The API does not apply exemptions: it returns a fee even for an alarmsõiduk, a diplomatic vehicle or a defence-sector vehicle. It also ignores the statutory cases where no fee arises (buying a leased car out of the lease when already the responsible user; registering an inherited vehicle) — check those against the act. - **No vehicle technical data, owner, restrictions or usage history.** "Sõiduki taustakontroll" (`https://eteenindus.mnt.ee/public/soidukTaustakontroll.jsf`) is gated by Google reCAPTCHA v3: a scripted POST gets the empty search form back, with no result and no error. Never present that empty form as a retrieval. The owner's own vehicle data needs ID-card / Mobiil-ID login in Transpordiamet e-teenindus, and third-party liiklusregister data is released only under `https://www.transpordiamet.ee/liiklusregistrist-juurdepaasupiiranguga-andmete-valjastamine`. - A plate or VIN identifies one person's car. Use those two endpoints only with an identifier the user supplied themselves. ## Verify - Require HTTP 200 with JSON containing numeric `totalPrice` and `basePrice`, and state the figure only from a printed response body. - HTTP 422 `{"errorMessage":"Vehicle not found"}` — the plate or VIN is not in the register (or plate and VIN belong to different vehicles). Report that, do not retry with a guessed identifier. ```probe-limit claim HTTP 422 `{"errorMessage":"Vehicle not found"}` — the plate or VIN is not in the register GET https://apimsm.transpordiamet.ee/v2/msm/regTasu/by-vehicle-identifier?vinCode=00000000000000000 expect-status 422 expect /Vehicle not found/ # A VIN, not a plate, because the claim covers both and the two are separate # lookups. The control prices a vehicle from technical parameters on the same # API: a 422-to-everything day must not read as "the register has no such VIN". control GET https://apimsm.transpordiamet.ee/v2/msm/regTasu/by-technical-parameters?category=M1&bodyworkCode=UNIVERSAAL&co2wltp=183&technPermMaxLadenMass=2790&initialRegDate=2021-01-12&seats=8®FeeCalcDate=2026-01-01 ``` A **placeholder** VIN is the exception and it fails quietly instead: `vinCode=XXXXXXXXXXXXXXXXX` (any run of `X`) answers HTTP 200 with every price `0.00` and `ageCoef: null` (checked 2026-08-02), which reads as "this car owes nothing" rather than "no such car". A total of zero next to a null `ageCoef` is not a fee — never state it as one, and never send a VIN the citizen did not give you. - HTTP 400 `{"errorMessage":"Invalid data"}` — a required parameter is missing. Name the missing parameter and ask; do not invent a mass, power or CO2 value. ```probe-limit claim HTTP 400 `{"errorMessage":"Invalid data"}` — a required parameter is missing GET https://apimsm.transpordiamet.ee/v2/msm/regTasu/by-technical-parameters?category=M1&initialRegDate=2021-01-12®FeeCalcDate=2026-01-01 expect-status 400 expect /Invalid data/ # Same request as the control, minus seats/co2wltp/technPermMaxLadenMass. If the # API ever starts pricing an incomplete parameter set, the number it returns is # computed from values nobody supplied — which is why this one is pinned. control GET https://apimsm.transpordiamet.ee/v2/msm/regTasu/by-technical-parameters?category=M1&bodyworkCode=UNIVERSAAL&co2wltp=183&technPermMaxLadenMass=2790&initialRegDate=2021-01-12&seats=8®FeeCalcDate=2026-01-01 ``` ## Module API registrationFee({category, initialRegDate, regFeeCalcDate, co2wltp, technPermMaxLadenMass, seats, engineType, ...}) -> {source, page, currency, totalPrice, basePrice, co2Price, massPrice, ageCoef, inputs, note}