GET /company/vat
Validates an intra-EU VAT number against VIES (VAT Information
Exchange System, European Commission) - the federation of the national VAT
registers of all EU member states (EU-27, plus XI for Northern Ireland).
The check is live: every answer reflects a query to the member state’s
register at the moment of the call. When that query does not get through - the
member state throttles or times out - and we already hold a firm verdict on
that number, we serve it back, dated by its age, rather than losing the answer
(the last firm verdict).
Given a French SIREN instead, the endpoint derives the French VAT number by
pure computation and checks the derived number the same way.
Use it as the pre-action check before an agent invoices a European
counterparty: an intra-EU VAT number that is confirmed by its member state is
what reverse-charge invoicing (intra-Community exemption) and invoice
mentions rely on. It pairs naturally with
GET /company/resolve (who the company is),
GET /company/events (what has legally happened
to it) and POST /invoice/validate, whose
plausibility checks are syntax-only by design - this endpoint is where
the register is actually consulted. See the live
/catalog for the authoritative endpoint listing and
price.
x402 golden rule: the agent pays for a verification that ran, not for
a “positive” result. All three outcomes - valid, invalid and
unverifiable - are 200, billed (see the three
outcomes below). Requests the service
cannot answer - a malformed number, a non-EU country prefix, a failed
checksum - leave the 200 range and are not billed.
Parameters
Provide exactly one of vat or siren - zero or both is a
400 INVALID_INPUT.
| Parameter | Type | Required | Description |
|---|---|---|---|
vat | string | one of | Full VAT number with its country prefix, any EU member state (EU-27 + XI) |
siren | string | one of | 9-digit French SIREN; the French VAT number is derived and then checked |
GET /company/vat?vat=FR40303265045
GET /company/vat?siren=303265045
Input is normalized before anything else: usual separators (spaces, dots,
dashes) are stripped, case is folded to upper, and the ISO alias GR is
accepted for the Greek VAT prefix EL. The number’s syntax is validated
per member state before any network call - a syntactically invalid number
is a 400, never a wasted VIES query.
French derivation from a SIREN
With siren=, the French VAT number is derived by pure computation:
key = (12 + 3 × (SIREN mod 97)) mod 97 → FR<key><SIREN>
For example SIREN 303265045 derives to FR40303265045. The SIREN must
first pass strict form (9 digits) and its Luhn check digit (no SIREN is
exempt - the La Poste SIREN 356000000 passes the standard check); a failed
checksum is a
400 INVALID_CHECKSUM - almost certainly a typo the agent should fix. The
derivation is traced in the answer (derived_from_siren) and in the
provenance. The same key consistency is enforced on vat=FR… input when the
key is numeric; alphabetic keys from the older French scheme are left for
VIES to judge.
200 response - UnifiedResponse
{
"data": { ... },
"provenance": {
"source": "vies",
"fetched_at": "2026-07-12T15:30:31Z",
"freshness": { "kind": "live" }
}
}
freshness.kind:liveon the nominal path - the answer is a VIES check at call time. It iscached(withage_secs) in one case only: the live check did not get through and we already had a firm verdict on this number within the last 24 hours, which is then served with the age it really has. There is no snapshot behind this endpoint.provenance.sourcestates what was actually consulted:vies(the member state answered),vies + pure-computation(same, with the French number derived from a SIREN), orpure-computationalone (degraded: VIES did not answer, only the local syntax check and derivation ran - the provenance never claims a consultation that did not happen).
Fields of data
| Field | Type | Description |
|---|---|---|
vat_number | string | The checked VAT number, normalized (derived when siren= was used) |
country | string | Member-state prefix of the number (FR, DE, EL, XI, …) |
syntax_valid | bool | The number respects its member state’s national syntax |
status | string | valid, invalid or unverifiable - see below |
name | string | Registered name, relayed only when the member state publishes it; absent otherwise |
address | string | Registered address, same publication rule; absent otherwise |
derived_from_siren | string | The input SIREN, present only when the number was derived from siren= |
reason | string | Only on unverifiable: why the register could not be consulted (timeout, network_unreachable, MS_UNAVAILABLE, …) |
retryable | bool | Only on unverifiable, served with reason: can asking the same question later produce a verdict? - see below |
coverage | string | Only on unverifiable: syntax_only - exactly what was verified |
checked_at | string | Timestamp of the check that produced this status - the original one when the verdict comes from cache, never the instant of the response |
served_from | string | cache when the live check failed and our last firm verdict was served instead; absent on a live answer |
cached_age_secs | number | Age of that cached verdict, in seconds; absent on a live answer |
The three outcomes, all paid answers
The question the agent pays for is “was this VAT number checked?” - and all three outcomes answer it:
valid- the member state’s register confirmed the number.nameandaddressare relayed as published when the state discloses them (some member states do not).invalid- the member state’s register firmly rejected the number. A “no” is a useful answer: the agent should not apply the intra-Community exemption to this counterparty.unverifiable- VIES or the member state was unavailable, and we had no firm verdict on this number to fall back on (see below). The degraded answer is honest about its own boundaries: the syntax check still ran (syntax_valid: true), the failure is motivated (reason), qualified (retryable) and the scope is bounded (coverage: "syntax_only") - the answer states exactly what was verified (the syntax) and what was not (the national register). “Unverifiable right now” is not “invalid” - and a VIES outage is never a 5xx on this endpoint.
Example - valid (real VIES answer, 2026-07-12)
GET /company/vat?vat=FR40303265045 (SODIMAS, the example documented by
VIES itself):
{
"data": {
"vat_number": "FR40303265045",
"country": "FR",
"syntax_valid": true,
"status": "valid",
"name": "SA SODIMAS",
"address": "11 RUE AMPERE\n26600 PONT DE L ISERE",
"checked_at": "2026-07-12T15:30:31.096065814Z"
},
"provenance": {
"source": "vies",
"fetched_at": "2026-07-12T15:30:31.096065814Z",
"freshness": { "kind": "live" }
}
}
Example - invalid, derived from a SIREN (real VIES answer, 2026-07-12)
GET /company/vat?siren=111111118 - a well-formed SIREN (Luhn passes) that
does not exist; the derived FR44111111118 is firmly rejected by the member
state:
{
"data": {
"vat_number": "FR44111111118",
"country": "FR",
"syntax_valid": true,
"status": "invalid",
"derived_from_siren": "111111118",
"checked_at": "2026-07-12T15:30:53.104349431Z"
},
"provenance": {
"source": "vies + pure-computation",
"fetched_at": "2026-07-12T15:30:53.104349431Z",
"freshness": { "kind": "live" }
}
}
This is a 200, billed: the question “is this VAT number valid?” received
its answer - a firm “no” from the member state. The derivation is traced
both in derived_from_siren and in the pure-computation part of the
source.
Example - unverifiable (degraded, VIES unreachable, 2026-07-12)
GET /company/vat?vat=FR40303265045 while VIES cannot be reached:
{
"data": {
"vat_number": "FR40303265045",
"country": "FR",
"syntax_valid": true,
"status": "unverifiable",
"checked_at": "2026-07-12T19:05:19Z",
"reason": "network_unreachable",
"retryable": true,
"coverage": "syntax_only"
},
"provenance": {
"source": "pure-computation",
"fetched_at": "2026-07-12T19:05:19Z",
"freshness": { "kind": "live" }
}
}
200, never a 5xx for a VIES outage: the syntax was verified
(syntax_valid: true), the degradation is motivated (reason), qualified
(retryable), bounded (coverage: "syntax_only"), and the provenance only
claims the local computation (pure-computation). The agent knows exactly what
it paid for - and whether asking again is worth anything.
(retryable is the one field added to this July capture; the field is newer
than the capture and the rest of it is unchanged.)
Is the gap worth re-asking? retryable
An unverifiable is served with retryable, and it answers one question:
would putting the same question again in a minute produce a verdict?
true- a throttle, a timeout, a network failure, anhttp_5xx, or our own restraint (local_vies_budget). Every one of those is a fact about VIES or about us, none of them a fact about the number you submitted.false- a firm refusal of the request (INVALID_INPUT,unexpected_response,http_4xx), and any reason we have never seen before. An unknown reason is qualifiedfalseon purpose: inviting a retry on a gap that does not close would make an agent pay a loop of calls for the same answer.
Without this field, unverifiable read as “this number cannot be verified” -
the most expensive conclusion, and the wrong one about half the time. The list
of retryable reasons is closed, and it is not the list of failures we retry
inside a single call: what we decline to re-emit right away (our own budget)
is still worth replaying later.
It is advice, not a promise: VIES is a free, shared public service, and it owes us nothing on the next call.
When VIES does not answer: the last firm verdict
VIES federates 27 national registers, and the quota of a member state is per member state, not per caller: a register can throttle a strictly sequential check simply because the rest of Europe is querying it at the same second. Losing an answer we already have to someone else’s traffic is the wrong trade, so when the live check fails - throttle, timeout, network - and we obtained a firm verdict on that exact number within the last 24 hours, that verdict is served, dated by its real age:
{
"data": {
"vat_number": "FR40303265045",
"country": "FR",
"syntax_valid": true,
"status": "valid",
"name": "SA SODIMAS",
"address": "11 RUE AMPERE\n26600 PONT DE L ISERE",
"checked_at": "2026-07-31T09:12:44Z",
"served_from": "cache",
"cached_age_secs": 8143
},
"provenance": {
"source": "vies",
"fetched_at": "2026-07-31T09:12:44Z",
"freshness": { "kind": "cached", "age_secs": 8143 }
}
}
Three guarantees make this answer buyable, and they are worth reading as guarantees:
- Nothing is invented. What you get is a fact really obtained from VIES,
dated to the moment it was obtained.
checked_atrewinds to the original check - we never claim to have queried the register at the second of your call - andserved_from/cached_age_secssit indata, visible without reading the provenance. - A fresh answer is never degraded. The memory is consulted only after the live check has failed, never instead of it; a check that succeeds refreshes the entry. You cannot get a dated verdict while a current one was available.
- An
unverifiablenever enters it. It is not a verdict. So the invariant holds both ways:cachedimplies a firmvalidorinvalid, carrying neitherreason, norretryable, norcoverage: "syntax_only"- those belong to the degraded answer, which is what you get when we have never had a verdict on this number.
And the limit, to know before you buy: a VAT verdict may be served from our
last firm check, up to 24 hours old. Watch freshness.age_secs (or
cached_age_secs) and decide for yourself whether that is recent enough for
what you are about to do - the memory is short-lived, holds no unverifiable,
and dies when the service restarts.
Freshness
This is a live endpoint: each answer is a VIES consultation at call time
(short timeout, a brief capped retry), and freshness.kind is live -
cached only on the fallback above, when the member state did not answer
and we hold a firm verdict less than 24 hours old. Nothing here is served from
a snapshot: a VAT registration can be revoked at any time, which is exactly why
a dated answer says its date instead of passing for a current one.
The fallback only ever helps on repetition. The first call on a given number is entirely live and pays the member state’s latency in full, and that latency belongs to the national register, not to this service. Measured on 12 August 2026, over two passes across ten member states:
- Most states answer between a few tens of milliseconds and two to four seconds, and the same number varies by an order of magnitude from one call to the next - a register answered in 173 ms on one pass and 1.2 s on the other. Do not budget on a single observation.
- French numbers are consistently the slowest: ten live checks in a row all landed between 7 and 10 seconds. If an agent’s own deadline is tighter than that, it will time out on France before this endpoint does.
- A register that stays silent is abandoned after about ten seconds and
yields
unverifiablewithreason: "timeout"- a bounded answer rather than a hanging call.
These figures are indicative, not a service level: they move with the load on each national register. That cost is upstream and it does not go away.
Limits
Stated plainly, so the agent knows what the answer does and does not claim:
- A verdict may be up to 24 hours old: on a member-state outage or
throttle, our last firm verdict on the number is served,
cachedand dated (age_secs). It is never a stale answer passed off as fresh - but it is not a check run for your call either. - Availability is per member state: VIES federates national registers,
so one state under maintenance yields
unverifiablefor its numbers only - checks against other states are unaffected. nameandaddressare relayed only when the member state publishes them; several states do not disclose them through VIES.- No consultation proof (VIES
requestIdentifier) in v1. siren=derivation applies to French companies only - and a valid SIREN does not imply VAT registration: the derived number can come backinvalidif the company is not registered for intra-Community VAT.
Errors
Only requests the service cannot answer leave the 200 range - none of
them are billed. Note the deliberate absence of a business 404: a
well-formed number always gets an answer (valid, invalid or
unverifiable).
| Status | code | Case |
|---|---|---|
| 400 | INVALID_INPUT | Zero or two of vat/siren; country prefix not an EU member state; number violates its state’s national syntax; malformed SIREN |
| 400 | INVALID_INPUT | Unknown query parameter name - the message names it, suggests the nearest expected one when there is a close match, and lists what the route accepts (vat, siren). Not charged. |
| 400 | INVALID_CHECKSUM | French VAT key inconsistent with the embedded SIREN, or siren= fails its Luhn check - a probable typo the agent should fix |
| 404 | NOT_FOUND | Unknown routes only - never a well-formed VAT number |
| 500 | INTERNAL | Internal error (detail logged, not exposed) - a VIES outage is not one of these |
{ "error": "provide exactly one of 'vat' or 'siren'", "code": "INVALID_INPUT" }
Attribution
Validation is performed against VIES (VAT Information Exchange System), operated by the European Commission - a public EU service, consulted without any key or account.
See also
GET /company/resolve- the official identity card behind a SIREN/SIRET: legal name, status, legal form, NAF activity, head office.GET /company/events- BODACC legal announcements for the same counterparty: insolvency proceedings, deregistrations, business sales.POST /invoice/validate- EN 16931 invoice validation; its VAT-number plausibility check is syntax-only, this endpoint is the register-level complement.- For agents - discovery surfaces, the live
/catalogand how settlement works.