What will a carte grise cost? French registration tax, by region
CO2 malus, weight malus and regional tax for a French registration, priced against the schedule in force on the date you supply and auditable against the law.
By Matthias Begot · · Updated
An agent costing a car purchase, an import or a fleet renewal in France needs
one number: what the carte grise will cost. That number is not a constant and
it is not something a language model can recall — it is the output of three
separate taxes, each read from a schedule that changes by finance law, and the
applicable schedule is the one in force on the registration date.
GET /vehicle/tax computes it at that date, shows the
bracket behind every euro, and names the articles of the code des impositions
sur les biens et services it applied. One x402 settlement, no account.
The schedule moves, and it does not wait for January
The tempting shortcut — ask a model, get a malus — fails on the calendar. The French CO2 malus threshold has moved three times in three finance laws, and the last move did not even land on a 1 January.
- Law n° 2025-127 of 14 February 2025 (loi de finances for 2025) set a new CO2 scale that entered into force on 1 March 2025, at a trigger threshold of 113 g/km — a mid-year switchover, because the law itself was promulgated in February.
- The same law programmed the threshold down 5 g each year: 108 g/km from 1 January 2026, 103 g/km in 2027.
- The weight malus followed its own track: its trigger threshold dropped from 1 600 kg to 1 500 kg in 2026.
So “the malus” is meaningless without a date, and a model trained before
February 2025 will confidently price a 2026 registration on a 2024 scale. The
endpoint takes registration_date and selects the schedule to the day,
including across that 1 March 2025 boundary. Omit it and you get today’s.
Three taxes, one call
A French registration certificate bills three things, and an agent that prices only the malus is short by the other two:
| Item | What it prices |
|---|---|
co2_malus | WLTP CO2 in g/km, priced gram by gram against the dated scale |
weight_malus | Mass in running order, at the marginal rate bracket by bracket after allowances |
regional_tax | Fiscal power × the tariff voted by the region, plus the flat national fee |
The regional part is the one most tools get wrong, because it is not national policy: each regional council votes its own tariff per fiscal horsepower, once a year. Two identical cars registered on the same day in two regions do not cost the same. And the flat national fee is levied on top of the proportional part.
GET /vehicle/tax?country=FR&co2_wltp=150&weight_kg=1900&fuel=diesel&fiscal_power=7®ion=11®istration_date=2026-05-01
Every euro shows its bracket
The weight malus is not a rate times a mass; it is a marginal scale. The response derives it in the open rather than asserting a total:
"weight_malus": {
"status": "due",
"amount_eur": 5500,
"mass_kg": 1900,
"taxable_mass_kg": 1900,
"threshold_kg": 1500,
"brackets": [
{ "from_kg": 1500, "to_kg": 1699, "taxable_kg": 200, "eur_per_kg": 10, "amount_eur": 2000 },
{ "from_kg": 1700, "to_kg": 1799, "taxable_kg": 100, "eur_per_kg": 15, "amount_eur": 1500 },
{ "from_kg": 1800, "to_kg": 1899, "taxable_kg": 100, "eur_per_kg": 20, "amount_eur": 2000 }
]
}
Three bracket lines, three multiplications, one sum. An agent can recompute the
5 500 EUR from the fields it was given — or show them to a human who wants to
know why. The CO2 item states its threshold_g and whether the amount was
capped; the regional item splits proportional_eur from fixed_fee_eur.
Auditable against the law, not against a blog post
The part that makes the figure defensible is schedule.legal_basis: every
applied article, with the code it belongs to, its number, the date it entered
into force, its subject — and its LEGI identifier.
{
"code": "code des impositions sur les biens et services",
"article": "L. 421-62",
"legi_id": "LEGIARTI000051214838",
"in_force_since": "2025-03-01",
"subject": "barèmes CO2 WLTP"
}
A real answer carries the ten or so articles behind the three items. That LEGI
identifier is a key into another endpoint:
GET /legal/article returns the text of that article
as in force, so an agent that must justify a figure can fetch the law it was
computed from instead of citing a tax calculator.
Read status before the amount
The three items share one closed enumeration, and the distinction between its values is the difference between “you owe nothing” and “we do not know”:
status | Meaning |
|---|---|
due | Computed — including a real 0 EUR below the threshold |
exempt | An automatic exemption applied, named by its article — never silent |
not_applicable | No schedule existed at that date (the weight malus before 2022) — an absence of tax |
undetermined | A required figure was missing: no amount, a reason instead |
Exemptions are cited, not implied. An electric car comes back with all three
items exempt, each carrying the article that exempts it — and the regional
item still bills a non-zero amount, because the flat national fee survives the
regional exemption. That is the law, and an endpoint that rounded it to zero
would be quietly wrong. A plug-in hybrid gets an allowance instead: a mass
deduction, with the kilograms it removed and an explicit exempts: false.
The total disappears rather than being partial
Drop co2_wltp from the request and the CO2 malus cannot be computed. What the
endpoint does not do is add up the other two and call it a total:
{
"co2_malus": {
"status": "undetermined",
"threshold_g": 108,
"reason": "'co2_wltp' is neither declared nor resolved: the CO2 malus cannot be computed"
},
"weight_malus": { "status": "due", "amount_eur": 5500 },
"regional_tax": { "status": "due", "amount_eur": 493.65 }
}
total_eur is absent from the JSON. Not null, not a partial sum — absent.
Every field that could be computed still is, so the agent keeps what it can use;
but a partial sum presented as a total is a wrong number, and a wrong number in
a costing pipeline propagates silently. The same discipline applies to coverage:
France only. Any other country is a 400 that says so, because
approximating a neighbouring schedule would be worse than refusing.
Declared, or resolved from the vehicle
The technical figures — CO2, mass, fuel — can be supplied directly or resolved
from a vehicle key. Pass a type-approval number, or a make/model/year, and they
come from the EU CO2 monitoring dataset behind
GET /vehicle/co2. inputs_used then labels each
figure declared or resolved, so the result stays reconstructable, and a
declared value always wins over a resolved one.
The administrative figures do not work that way. Fiscal power and region are French records absent from the EU registry, so they are always declarative.
Resolution also propagates its own precision. A make/model/year key matches a
whole model-year population, and the answer says so: provenance.source becomes
fr-registration-tax+eea-co2-monitoring, and limits gains a line stating the
CO2 came from a model-year aggregate spanning N versions — so the malus of one
specific version may differ. As the CO2 article
shows, a single commercial name can span a battery electric variant at 0 g/km
and a petrol version well over the malus threshold. That uncertainty is
propagated, never hidden behind a clean-looking euro amount.
What it refuses to do
Six reading limits come back with every answer. The load-bearing ones:
- New vehicles registered in France. The reduction coefficient for vehicles already registered abroad — imported used cars — is not modelled.
- WLTP schedules only. NEDC and administrative-power scales for vehicles outside WLTP are out.
- Exemptions that depend on the taxpayer, not the vehicle (disability card, dependent children, a legal person operating a vehicle with at least eight seats) are published in the schedules but never applied automatically — the service does not know who is buying.
- The regional tariff is voted yearly and applies at the date the certificate is issued, not at first registration; only current tariffs are published, so there is no regional history to query.
- Out of scope: annual taxes on business use, transport-vehicle taxes, purchase incentives and the non-tax delivery fee.
This is a costing of the published schedules. It is not tax advice, and it is not a quote from the administration.
Where it sits in the x402 loop
- Discover the endpoint in the live catalog, call it, receive the
402. - Pay — sign the chosen rail and replay the request.
- Read
statuson each item before reading any amount. - Branch — commit the total, ask a human when an item is
undetermined, or go back for a precise type-approval number when the CO2 was a model-year aggregate.
Per the x402 golden rule, the agent pays for the answer to its question. A
computable request is a billed 200, including one where an item is exempt,
not_applicable or undetermined — those are answers. Only requests the
service cannot answer — a country other than France, a date before the first
published schedule, an unknown region, a missing fuel — leave the 200 range and
are not billed. Quickstart walks the full discover → 402 →
pay → replay cycle; price and accepted rails are served live by the
catalog, never pinned in an article.
Chain it
GET /vehicle/co2 supplies the CO2 and mass this
costing is priced on, and states the precision it matched them at.
GET /vehicle/vin/decode turns a VIN from a
listing into the make, model and year that key that lookup. For a running-cost
picture rather than a purchase cost,
GET /vehicle/critair answers the low-emission-zone
question from the same fuel vocabulary. And for all of it against one VIN in a
single settlement, use the composite
GET /vehicle/report.
For the full field reference, the resolution keys and the error codes, see the
GET /vehicle/tax documentation; for how agents
discover and call Invoket endpoints, see For agents.