Is this EU VAT number valid right now? A live VIES check
Before an agent invoices cross-border, GET /company/vat asks the member state live via VIES. Valid, invalid and unverifiable are all paid, dated answers.
By Matthias Begot · · Updated
An agent about to issue a cross-border invoice inside the EU has to decide
whether to charge VAT or apply the intra-Community exemption, and that decision
rests on a number the counterparty typed into a form. Since 1 January 2020 the
customer’s VAT identification number is not a formality on that invoice — it is
a substantive condition of the exemption itself.
GET /company/vat puts the question to the member
state’s own register, live, at the moment of the call, and returns one of three
outcomes — valid, invalid, unverifiable. All three are paid 200s,
because the agent pays for a check that ran, not for a “positive” result.
This article walks the endpoint and, more importantly, its boundaries, with captures taken from production on 12 August 2026.
Why the number stopped being a formality
Council Directive (EU) 2018/1910 of 4 December 2018 — the “Quick Fixes” — amended Article 138(1) of the VAT Directive 2006/112/EC and added two substantive conditions to the exemption of an intra-Community supply of goods, applicable from 1 January 2020:
- the acquirer is identified for VAT purposes in a member state other than the one of dispatch, and has indicated that VAT identification number to the supplier;
- the supplier files a correct recapitulative statement (EC Sales List) covering the supply.
Before that change, a missing or wrong VAT number was a formal defect that could often be cured after the fact. After it, the number is part of what makes the exemption exist. That is what turns “check the VAT number” from bookkeeping hygiene into a pre-action check — a thing an agent must do before it issues the document, not during a later reconciliation.
VIES is not a database — it is 27 registers behind one door
The European Commission is explicit that VIES is a search engine, not a database: it forwards each query to the register of the member state concerned and relays that state’s reply. The Commission accepts no responsibility for the accuracy of data it does not hold.
Three consequences follow, and every one of them shows up in the response:
- Availability is per member state. One national register under maintenance or backup affects its numbers only; checks against the other states are unaffected. Your Europe states the cause plainly — “the system is sometimes not available due to the need to back-up the national databases”.
- The quota is per member state, not per caller.
MS_MAX_CONCURRENT_REQis a documented VIES condition meaning the maximum number of concurrent requests for that state has been reached. A strictly sequential, well-behaved caller can be throttled simply because the rest of Europe is querying the same register in the same second. - Disclosure varies. Some states publish the registered name and address through VIES; others return only the verdict. This is not a gap in the endpoint — it is the member state’s own policy, relayed as-is.
An integration that treats VIES as a uniform, always-on EU service will be surprised. An integration that treats it as a federation of national registers with national behaviours will not.
The three outcomes, all of them answers
valid — the register confirmed it
GET /company/vat?vat=IT00159560366:
{
"data": {
"vat_number": "IT00159560366",
"country": "IT",
"syntax_valid": true,
"status": "valid",
"name": "FERRARI S.P.A.",
"address": "VIA EMILIA EST 1163 \n41122 MODENA MO",
"checked_at": "2026-08-12T06:14:39.751919101Z"
},
"provenance": {
"source": "vies",
"fetched_at": "2026-08-12T06:14:39.751919101Z",
"freshness": { "kind": "live" }
}
}
The same call against a German number, in the same session, comes back confirmed with no name and no address:
{
"data": {
"vat_number": "DE811907980",
"country": "DE",
"syntax_valid": true,
"status": "valid",
"checked_at": "2026-08-12T06:19:13.790857354Z"
},
"provenance": {
"source": "vies",
"fetched_at": "2026-08-12T06:19:13.790857354Z",
"freshness": { "kind": "live" }
}
}
Both are complete answers. An agent that requires name to be present in order
to proceed has written a rule that fails on entire member states.
invalid — the register firmly rejected it
A firm “no” is a useful answer, and it is worth being precise about what it means. Your Europe gives the definition: an invalid status means the number “is not registered in the relevant national database”, for one of three reasons — the number does not exist, it has not been activated for intra-EU transactions, or the registration is not yet finalised.
That second reason is the one that catches integrations. A company can be real, trading and perfectly in order domestically while its VAT number is simply not enabled for intra-Community transactions. So:
invalidmeans this number is not usable for the intra-EU exemption. It does not mean the company does not exist.
Those are two different questions, answered by two different registers. If what
the agent actually needs to know is whether the legal entity exists and is
still active, that is
GET /company/resolve against INSEE Sirene — and
whether anything legal has happened to it is
GET /company/events against BODACC. Conflating
them produces the worst kind of agent decision: a supplier refused because a
tax flag was read as an existence check.
unverifiable — nobody answered, and the response says so
{
"data": {
"vat_number": "FR40303265045",
"country": "FR",
"syntax_valid": true,
"status": "unverifiable",
"checked_at": "2026-08-12T06:14:27.288031693Z",
"reason": "MS_MAX_CONCURRENT_REQ",
"coverage": "syntax_only"
},
"provenance": {
"source": "pure-computation",
"fetched_at": "2026-08-12T06:14:27.288031693Z",
"freshness": { "kind": "live" }
}
}
Read what this response refuses to do. It does not round unverifiable down to
invalid, which would silently kill a legitimate exemption. It does not round
it up to valid, which would sign off on a supply that was never checked. It
does not raise a 5xx and lose the work. It states the failure
(reason), bounds the scope (coverage: "syntax_only"), and — the detail that
matters most — the provenance drops to pure-computation, because claiming
vies as a source when VIES did not answer would be a lie inside a paid
response.
A VIES outage is never a 5xx on this endpoint. It is a dated, motivated,
bounded 200.
When the register does not answer: the last firm verdict
Losing an answer you already hold to someone else’s traffic is a bad trade. So when the live check fails and a firm verdict on that exact number was obtained within the last 24 hours, that verdict is served — dated by its real age. Fired six times concurrently at the same French number, production returned three live answers and three served from memory:
{
"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-08-12T06:18:12Z",
"served_from": "cache",
"cached_age_secs": 157
},
"provenance": {
"source": "vies",
"fetched_at": "2026-08-12T06:18:12Z",
"freshness": { "kind": "cached", "age_secs": 157 }
}
}
Three invariants make that answer buyable:
- Nothing is invented. It is a fact really obtained from VIES, and
checked_atrewinds to the moment it was obtained — the response never claims to have queried the register at the second of your call. - A fresh answer is never degraded. Memory is consulted only after the live check has failed, never instead of it. You cannot receive a dated verdict while a current one was available.
- An
unverifiablenever enters it. It is not a verdict. Socachedimplies a firmvalidorinvalid, and carries neitherreasonnorcoverage.
The limit is stated so an agent can act on it: a verdict may be up to 24
hours old. Read freshness.age_secs and decide whether that is recent enough
for what you are about to do.
From a SIREN, without a network call
French callers rarely hold the VAT number — they hold the SIREN. Pass
siren= and the French VAT number is derived by pure computation, then checked
like any other:
key = (12 + 3 × (SIREN mod 97)) mod 97 → "FR" + key + SIREN
{
"data": {
"vat_number": "FR40303265045",
"country": "FR",
"syntax_valid": true,
"status": "valid",
"name": "SA SODIMAS",
"address": "11 RUE AMPERE\n26600 PONT DE L ISERE",
"derived_from_siren": "303265045",
"checked_at": "2026-08-12T06:16:27.608979587Z"
},
"provenance": {
"source": "vies + pure-computation",
"fetched_at": "2026-08-12T06:16:27.608979587Z",
"freshness": { "kind": "live" }
}
}
The derivation is traced twice — in derived_from_siren and in the
pure-computation half of the source — so the agent can always tell a number it
supplied from a number we computed. And the obvious trap is worth naming: a
valid SIREN does not imply VAT registration. siren=111111118 is well-formed,
passes its Luhn check, derives cleanly to FR44111111118, and comes back
invalid from the register.
What is a 4xx, and therefore unbilled
Only requests the service cannot answer leave the 200 range, and none of them are billed. Syntax is validated per member state before any network call, so a malformed number never becomes a wasted VIES query:
| Request | Result |
|---|---|
neither or both of vat / siren | 400 INVALID_INPUT |
vat=US123456789 | 400 INVALID_INPUT — not an EU prefix |
vat=DE1234 | 400 INVALID_INPUT — violates the DE national format |
vat=FR99303265045 | 400 INVALID_CHECKSUM — key inconsistent with its SIREN |
siren=303265046 | 400 INVALID_CHECKSUM — fails Luhn |
Note the deliberate absence of a business 404: a well-formed number always
gets an answer. “Unknown” is not one of the outcomes, because the register — not
this service — decides what it knows.
Where it sits in an agent’s invoicing loop
The discovery-to-settlement mechanics are the standard x402 loop described in
the quickstart and for agents: call,
receive a 402 carrying the terms, pay, replay. What matters here is the
sequencing around it.
- Determine the treatment.
POST /invoice/vat-treatmentdecides who charges the VAT, under which mechanism, and which compulsory mentions to print. It takes the customer’s VAT number as declared, with zero network access, by design. - Verify the number that treatment relied on. That is this endpoint. The split is intentional: a determination engine should be deterministic and offline; the register consultation is a separate, live, dated act.
- Keep the dated response as the evidence. A
validis an attestation at an instant, not a standing property — a registration can be revoked the day after. Store the response with itschecked_at.
If the agent needs identity, legal events, VAT and e-invoicing reachability in a
single settlement rather than four,
GET /company/report composes them by SIREN, each
block carrying its own provenance. The live /catalog is
authoritative for the endpoint listing and pricing — never hardcode either.
Limits, stated plainly
- A verdict may be served from our last firm check, up to 24 hours old,
always flagged
cachedand dated. It is never passed off as fresh — but it is not a check run for your call either. - Availability is per member state: one state down yields
unverifiablefor its numbers only. nameandaddressare relayed only when the member state publishes them. Several do not.- No consultation proof (the VIES
requestIdentifier) in v1. - No name/address consistency check. VIES can confirm whether a given name
matches a number; this endpoint relays what is published, it does not score a
match. Payee-name matching on the banking side is a different question —
see
POST /preflight/payment. siren=derivation is French only, and derivation is arithmetic, not registration.
See also
GET /company/vat— full parameters, response fields and error codes.GET /company/resolve— does this legal entity exist and is it still active?GET /company/events— BODACC insolvency, deregistration and business-sale announcements.POST /invoice/vat-treatment— who charges the VAT, and the exact mentions to print.- Quickstart — the
402→ pay → replay loop end to end.