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:

  1. 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;
  2. 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_REQ is 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:

invalid means 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_at rewinds 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 unverifiable never enters it. It is not a verdict. So cached implies a firm valid or invalid, and carries neither reason nor coverage.

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:

RequestResult
neither or both of vat / siren400 INVALID_INPUT
vat=US123456789400 INVALID_INPUT — not an EU prefix
vat=DE1234400 INVALID_INPUT — violates the DE national format
vat=FR99303265045400 INVALID_CHECKSUM — key inconsistent with its SIREN
siren=303265046400 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.

  1. Determine the treatment. POST /invoice/vat-treatment decides 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.
  2. 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.
  3. Keep the dated response as the evidence. A valid is an attestation at an instant, not a standing property — a registration can be revoked the day after. Store the response with its checked_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 cached and 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 unverifiable for its numbers only.
  • name and address are 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