Skip to content
lexratesAccount

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 required

One call, and the answer carries its own sources.

a figure, on a date the open tier covers
curl "https://api.lexrates.com/v1/compute?regime=gb-amap&vehicleClass=car-van&units=1000&on=2024-06-01"
what comes back · 200
{
  "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"
}
the same call inside the last twelve months
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"
# → 200

A 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 · knownOn

Ask 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.
reproduce a February run, before a retroactive text existed
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
Every endpoint the API serves, generated from its OpenAPI document
PathWhat it answersQuery
/v1Service index–
/v1/catalogue· csvEvery regime of every domain, with days remaining before its figures expire
  • domain
  • format
/v1/regimes/{id}One regime and its periods
  • id*
/v1/countries/{code}Every regime published in one country
  • code*
/v1/computeApply a regime to a distance on a date
  • regime*
  • vehicleClass*
  • units*
  • on
  • knownOn
  • modifiers
  • passengers
  • alreadyClaimed
  • paidPerUnit
  • use
/v1/per-diem/computeA day of per-diem meal allowance
  • regime*
  • paidPerDay
  • kind*
  • on
  • knownOn
  • hours
  • meals
  • use
  • destination
/v1/changes· csvEvery date a figure moved
  • since
  • format
/v1/alertsWhat is about to expire
  • within
/v1/openapi.jsonThis document–
/healthLiveness–

* 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 time

The 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.

per-diem, lunch provided · amount 30
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
# = 30

The 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.

what an employer paid, against the scale · 450 GBP approved
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 unclaimed

Two 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.

the change log, as a spreadsheet
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.

what is about to move
curl -H "Authorization: Bearer YOUR_KEY" "https://api.lexrates.com/v1/alerts?within=60"

curl "https://api.lexrates.com/v1/alerts"
# → 402, with no key

The 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
Every refusal the API can return
StatusTypeWhen
404not-foundThe id or country code in the path is not one this corpus publishes.
400bad-requestA 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.
402outside-the-open-windowThe date asked about falls inside the last twelve months, or knownOn asks what was known after the cut, which is a question about today.
404no-figure-in-forceThe regime exists and holds no period covering that date.
404no-national-figureThe country publishes no single rate, Italy sets it per vehicle model in the ACI tables.
400wrong-domainA per-diem regime was sent to /v1/compute, or a mileage regime to /v1/per-diem/compute.
401key-requiredAn 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.
401invalid-keyA 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.
405method-not-allowedThe path is right and the method is not. This API is read-only.
503budget-exceededA per-caller or service-wide ceiling was reached. The ceilings exist because a public endpoint spends unattended; they are stated on the pricing page.
503service-unconfiguredA 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.
500internal-errorSomething 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 printed

The 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.

Get a key