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.

ParameterTypeRequiredDescription
vatstringone ofFull VAT number with its country prefix, any EU member state (EU-27 + XI)
sirenstringone of9-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: live on the nominal path - the answer is a VIES check at call time. It is cached (with age_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.source states what was actually consulted: vies (the member state answered), vies + pure-computation (same, with the French number derived from a SIREN), or pure-computation alone (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

FieldTypeDescription
vat_numberstringThe checked VAT number, normalized (derived when siren= was used)
countrystringMember-state prefix of the number (FR, DE, EL, XI, …)
syntax_validboolThe number respects its member state’s national syntax
statusstringvalid, invalid or unverifiable - see below
namestringRegistered name, relayed only when the member state publishes it; absent otherwise
addressstringRegistered address, same publication rule; absent otherwise
derived_from_sirenstringThe input SIREN, present only when the number was derived from siren=
reasonstringOnly on unverifiable: why the register could not be consulted (timeout, network_unreachable, MS_UNAVAILABLE, …)
retryableboolOnly on unverifiable, served with reason: can asking the same question later produce a verdict? - see below
coveragestringOnly on unverifiable: syntax_only - exactly what was verified
checked_atstringTimestamp of the check that produced this status - the original one when the verdict comes from cache, never the instant of the response
served_fromstringcache when the live check failed and our last firm verdict was served instead; absent on a live answer
cached_age_secsnumberAge 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. name and address are 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, an http_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 qualified false on 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_at rewinds to the original check - we never claim to have queried the register at the second of your call - and served_from / cached_age_secs sit in data, 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 unverifiable never enters it. It is not a verdict. So the invariant holds both ways: cached implies a firm valid or invalid, carrying neither reason, nor retryable, nor coverage: "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 unverifiable with reason: "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, cached and 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 unverifiable for its numbers only - checks against other states are unaffected.
  • name and address are 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 back invalid if 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).

StatuscodeCase
400INVALID_INPUTZero or two of vat/siren; country prefix not an EU member state; number violates its state’s national syntax; malformed SIREN
400INVALID_INPUTUnknown 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.
400INVALID_CHECKSUMFrench VAT key inconsistent with the embedded SIREN, or siren= fails its Luhn check - a probable typo the agent should fix
404NOT_FOUNDUnknown routes only - never a well-formed VAT number
500INTERNALInternal 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 /catalog and how settlement works.