API · 10 endpoints · verified 2026-11-04
Read-only, and it answers without a key.
Everything the corpus knew twelve months ago is open: no account, no key, no rate agreement. A key adds the current year, the future and the alert. Every response is JSON, and the catalogue and the change log also answer CSV; every refusal is an RFC 9457 problem document with a URL you can follow.
base https://api.lexrates.com
Start here
no key requiredOne call, and the answer carries its own sources.
curl "https://api.lexrates.com/v1/compute?regime=gb-amap&vehicleClass=car-van&units=1000&on=2024-06-01"{
"regime": "gb-amap",
"country": "GB",
"on": "2024-06-01",
"amount": 450,
"currency": "GBP",
"unit": "mi",
"billableUnits": 1000,
"steps": [
"1000 × 0.45 = 450"
],
"sources": [
{
"publisher": "gov.uk",
"url": "https://www.gov.uk/government/publications/rates-and-allowances-travel-mileage-and-fuel-allowances/travel-mileage-and-fuel-rates-and-allowances",
"verifiedOn": "2026-09-05"
},
{
"publisher": "gov.uk, Claim tax relief for your job expenses: vehicles you use for work",
"url": "https://www.gov.uk/tax-relief-for-employees/vehicles-you-use-for-work",
"verifiedOn": "2026-09-02"
}
],
"nextReviewDue": "2027-04-05"
}curl "https://api.lexrates.com/v1/compute?regime=gb-amap&vehicleClass=car-van&units=1000&on=2026-06-01"
# → 402, naming the most recent open period
curl -H "Authorization: Bearer YOUR_KEY" \
"https://api.lexrates.com/v1/compute?regime=gb-amap&vehicleClass=car-van&units=1000&on=2026-06-01"
# → 200A key in the query string works too and is the weaker form: ?key= travels into CDN logs, browser history and cache keys, where a header does not. Prefer the header.
YOUR_KEY comes from your account: create one, subscribe, and the key is on that page from then on. It is attached to the account rather than handed over once, so losing it is not an incident. What the subscription costs, and what the open tier gives you without one, is on the pricing page.
The two time axes
on · knownOnAsk what applied, or ask what you could have known.
- on
- The date the journey happened, or the day being paid. Absent, today. This is the axis everybody expects.
- knownOn
- The date you are pretending to stand on. Absent, everything known is used. Set it to reproduce a run you made in the past: a text published in March and retroactive to January did not exist in February, and a reconciliation that ignores that reads as a defect in your product rather than in the gazette.
curl "https://api.lexrates.com/v1/compute?regime=nz-ird-kilometre-rates&vehicleClass=petrol\
&units=500&on=2025-09-01&knownOn=2026-01-01"
# → 404 no-figure-in-force: that instrument was made in June 2026.Asking knownOn explicitly is a paid question, whatever the answer would have been: asking what was published after the open-tier line is a question about today.
Endpoints
10 documented| Path | What it answers | Query |
|---|---|---|
| /v1 | Service index | – |
| /v1/catalogue· csv | Every regime of every domain, with days remaining before its figures expire |
|
| /v1/regimes/{id} | One regime and its periods |
|
| /v1/countries/{code} | Every regime published in one country |
|
| /v1/compute | Apply a regime to a distance on a date |
|
| /v1/per-diem/compute | A day of per-diem meal allowance |
|
| /v1/changes· csv | Every date a figure moved |
|
| /v1/alerts | What is about to expire |
|
| /v1/openapi.json | This document | – |
| /health | Liveness | – |
* required · csv also answers ?format=csv
Only two paths serve CSV, and that is a decision rather than an omission: a statutory figure can be a flat rate, marginal bands, a bracket formula with a fixed part, or a pair of day rates across hundreds of destinations. A CSV of the figures would be one number per row, which is the shape this product exists to say is wrong. The catalogue and the change log are genuinely tabular, so they serve it.
Both answers begin with a UTF-8 byte order mark. Regime names carry accents and a spreadsheet opening the file from disk does not read the charset header, so without it Indemnité kilométrique arrives mangled. Reading the file in code, pass encoding="utf-8-sig" or the equivalent.
https://api.lexrates.com/v1/openapi.json : generate a client from it.
Worked answers
asked at build timeThe rest of what it does, each shown answering.
A meal deduction is a share of the full day, whatever day is being paid. Taking it of the day being paid halves it on every arrival and departure of every trip, which is the single most common way this arithmetic goes wrong.
curl "https://api.lexrates.com/v1/per-diem/compute?regime=de-auslandstagegelder&destination=jp-tokio&kind=full&meals=lunch&on=2025-06-15"
# full day: 50
# lunch provided: 40% of the full day 50 = −20
# = 30The statutory figure is a ceiling, not an instruction. Send what the employer actually paid and the answer splits it: what is within the scale, what is above it, and belowScale, which is what an employee paid under the scale claims relief on.
curl "https://api.lexrates.com/v1/compute?regime=gb-amap&vehicleClass=car-van&units=1000&paidPerUnit=0.2&use=tax-deduction&on=2024-06-01"
# 1000 × 0.45 = 450
# paid 1000 × 0.2 = 200
# 200 within the scale, 250 of it unclaimedTwo endpoints answer CSV, and only two: /v1/catalogue and /v1/changes. Everywhere else it is a 400 naming them, because a statutory figure is not a row and a spreadsheet of them would be wrong in the format most likely to be pasted into a payslip.
curl -sG "https://api.lexrates.com/v1/changes" -d since=2024-01-01 -d format=csv
# date,country,regimeId,regime,line,lineLabel,from,to,publishedOn,ahead,valuesWithheld
# 2026-10-01,BE,be-indemnite-kilometrique-bosa,"Indemnité kilométrique, fonction publique fédérale (BOSA)",car,Véhicule personnel,,,,false,true
# 2026-07-01,AU,au-ato-cents-per-km,ATO cents per kilometre method,car,Car,,,,false,true
# …And the alert, which is the one thing neither comparable product sells: which figures are about to need re-reading, which regimes have already gone quiet, and when coverage runs out. It is a paid question in every form.
curl -H "Authorization: Bearer YOUR_KEY" "https://api.lexrates.com/v1/alerts?within=60"
curl "https://api.lexrates.com/v1/alerts"
# → 402, with no keyThe same list, without a key and without a request, is the Atom feed: dates open to everyone, amounts inside the last twelve months withheld.
When it refuses
12 named failures| Status | Type | When |
|---|---|---|
| 404 | not-found | The id or country code in the path is not one this corpus publishes. |
| 400 | bad-request | A parameter is missing, blank, out of range, or not a value this endpoint accepts. A blank parameter is malformed rather than zero: ?units= means a field somebody meant to fill. |
| 402 | outside-the-open-window | The date asked about falls inside the last twelve months, or knownOn asks what was known after the cut, which is a question about today. |
| 404 | no-figure-in-force | The regime exists and holds no period covering that date. |
| 404 | no-national-figure | The country publishes no single rate, Italy sets it per vehicle model in the ACI tables. |
| 400 | wrong-domain | A per-diem regime was sent to /v1/compute, or a mileage regime to /v1/per-diem/compute. |
| 401 | key-required | An account path was called with no credential. Unlike /v1, these have no open tier: they answer about one subscription and cannot do that without knowing which. |
| 401 | invalid-key | A credential was presented and cannot be read, or is not a key this service knows. Sending an unreadable Authorization header counts: it is never treated as an anonymous request. |
| 405 | method-not-allowed | The path is right and the method is not. This API is read-only. |
| 503 | budget-exceeded | A per-caller or service-wide ceiling was reached. The ceilings exist because a public endpoint spends unattended; they are stated on the pricing page. |
| 503 | service-unconfigured | A deployment is missing one of the settings this service refuses to run without: its spend ceiling, its key store or its identity provider. Every path answers this, including /health, and the pages of the site keep serving. |
| 500 | internal-error | Something this service depends on did not answer, or a handler has a fault. It is not a fault in your request, and it is not the deployment being unconfigured, which answers 503. |
Each type is a URL that resolves to a page explaining the failure and what to do about it. That is what RFC 9457 asks the field to be, and it is checked in both directions: no refusal can name a page that does not exist, and no page can document a refusal the API never returns.
What it costs to call
enforced, not printedThe ceiling is in the code, and these are the numbers it enforces.
- Anonymous
- 1,000
- requests a day, per caller. The open tier is meant to be sampled, not mirrored.
- With a key
- 100,000
- requests a day.
- Whole service
- 2,000,000
- requests a month. A stop against a runaway client, not a billing meter.
Every answer carries x-ratelimit-limit and x-ratelimit-remaining, refusals included: a request that is turned away still spends. Reach the ceiling and the answer is a 503 whose Retry-After says when the window rolls. A closed period is settled arithmetic over settled figures, so it is cacheable for an hour and carries an ETag. An answer made with a key is private: no shared cache may store it, and your own client may hold it for 300 seconds.