EU VAT number check API: VIES REST, error codes and what it cannot tell you
The European Commission checks EU VAT numbers for free through VIES, with no key and no bulk download; this guide shows the REST call, the failure modes you must retry, and what a "valid" answer does not mean. Source: Arcadium Data, data as of .
VIES (VAT Information Exchange System) is the only official way to learn whether an EU VAT number is registered right now. It does not hold the data itself: every request is forwarded to the tax authority of the member state in the number's prefix, and the answer comes back through the Commission. That one fact explains most of what goes wrong in production. The service is as available as the slowest national back end, and the amount of detail you get depends on the country.
This guide starts with the official route, including its limits. It then shows how to get the same check plus the national register status in one call from the Arcadium Data API.
The official route: VIES
Endpoints
The Commission offers a web form, a SOAP service (WSDL at ec.europa.eu/taxation_customs/vies/checkVatService.wsdl) and a REST API. The REST API is the simplest for new code:
GET https://ec.europa.eu/taxation_customs/vies/rest-api/ms/{country}/vat/{number}: one number, with the country prefix and the rest of the number as separate path parts.POST https://ec.europa.eu/taxation_customs/vies/rest-api/check-vat-numberwith a JSON body (countryCode,vatNumber). This form also accepts the requester's own VAT number when you need a consultation reference for your records.
No account or key is needed. In the responses we read, the JSON carries a validity flag (isValid), the date of the request, a userError text and, for most member states, the registered name and address. Check a live response for your own number before you rely on a field name; the Commission may change the schema.
curl
# Ireland: country prefix IE, number 6388047V
curl -s https://ec.europa.eu/taxation_customs/vies/rest-api/ms/IE/vat/6388047V
Python
import time
import requests
RETRY = {"MS_UNAVAILABLE", "MS_MAX_CONCURRENT_REQ", "TIMEOUT", "SERVICE_UNAVAILABLE"}
def vies_check(vat: str, tries: int = 4):
"""vat = 'IE6388047V' (prefix + number). Returns the VIES JSON, or None if VIES kept failing."""
country, number = vat[:2].upper(), vat[2:].replace(" ", "")
url = "https://ec.europa.eu/taxation_customs/vies/rest-api/ms/" + country + "/vat/" + number
for attempt in range(tries):
r = requests.get(url, timeout=30)
if r.ok:
data = r.json()
if data.get("userError") not in RETRY:
return data
time.sleep(2 ** attempt) # the member state is down or busy: back off
return None
print(vies_check("IE6388047V"))
TypeScript
const RETRY = new Set(['MS_UNAVAILABLE', 'MS_MAX_CONCURRENT_REQ', 'TIMEOUT', 'SERVICE_UNAVAILABLE']);
export async function viesCheck(vat: string, tries = 4): Promise<Record<string, unknown> | null> {
const country = vat.slice(0, 2).toUpperCase();
const number = vat.slice(2).replace(/\s/g, '');
const url = 'https://ec.europa.eu/taxation_customs/vies/rest-api/ms/' + country + '/vat/' + number;
for (let attempt = 0; attempt < tries; attempt++) {
const res = await fetch(url, { signal: AbortSignal.timeout(30_000) });
if (res.ok) {
const data = (await res.json()) as Record<string, unknown>;
if (!RETRY.has(String(data.userError))) return data;
}
await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
}
return null;
}
Prefixes and number formats
Validate the shape before you call VIES; a malformed number wastes a request against a national back end that may already be struggling. The prefix is the ISO country code with two exceptions: Greece uses EL, not GR, and Northern Ireland uses XI (for goods only). Great Britain (GB) left VIES after Brexit, so a GB number cannot be checked here; HMRC runs its own check service for those.
| Country | Prefix | Shape after the prefix |
|---|---|---|
| Germany | DE | 9 digits |
| France | FR | 2 check characters + the 9-digit SIREN |
| Italy | IT | 11 digits |
| Spain | ES | 9 characters (letters allowed at either end) |
| Netherlands | NL | 9 digits + "B" + 2 digits |
| Poland | PL | 10 digits |
| Belgium | BE | 10 digits |
| Austria | AT | "U" + 8 digits |
| Ireland | IE | 7 digits + 1 or 2 letters (older numbers differ) |
Failure modes that are not "invalid"
VIES separates "this number is not registered" from "I could not ask". Treat the second as a retry, never as a rejection of your customer. The codes we see most are MS_UNAVAILABLE (the member state's system is down, often at night or during maintenance), MS_MAX_CONCURRENT_REQ (the member state refuses parallel load), TIMEOUT and a plain HTTP error from the gateway. Use a queue with exponential backoff, send one request at a time per country, and show "could not verify yet" in your product. There is no published per-key quota because there are no keys; the practical limit is how much the national back ends tolerate.
What "valid" does and does not mean
- It means the number is VAT-registered today. It does not mean the company is trading, solvent or free of insolvency proceedings. A company in liquidation can keep its registration.
- Name and address depend on the country. Several states return them; Germany and Spain answer with
---instead. Do not build a "name matches" rule that needs the name from every country. - Sole traders carry a VAT number too. The name field can then be a private person, which is personal data under the GDPR. Store it only if you have a lawful basis.
- There is no search by name, no bulk file and no history. VIES answers one number at a time, as of now.
- For proof in a tax dispute, use the consultation-number route (the POST call with your own VAT number) and keep the reference. A plain GET is a lookup, not evidence.
Reuse of the VIES output is covered by Commission Decision 2011/833/EU.
The fast route: one call that adds the register
The Arcadium Data route GET /v1/companies/verify calls VIES for you, handles the failure codes, caches the answer for 24 hours and, where the national register is open and anonymous, adds the register status in the same response. Today those registers are Norway (Brønnøysund), Finland (PRH/YTJ), France (recherche-entreprises plus stored BODACC insolvency notices) and the Czech Republic (ARES). For every other country the answer rests on the VAT registration alone and says so in statusBasis.
Calls on this page work without a key from one IP (10 requests a minute, 100 a day, short answers, point lookups). A free key lifts the limits: sign up. Routes, parameters and fields: OpenAPI.
curl
curl -s "https://api.arcadiumdata.io/v1/companies/verify?vat=IE6388047V"
Response (abridged)
{
"status": "active",
"statusBasis": "vat_registration",
"country": "IE",
"vatNumber": "IE6388047V",
"legalName": "GOOGLE IRELAND LIMITED",
"vat": {
"vatNumber": "IE6388047V",
"valid": true,
"error": null,
"requestDate": "2026-10-25T06:56:41.225Z",
"source": "vies",
"sourceUrl": "https://ec.europa.eu/taxation_customs/vies/rest-api/ms/IE/vat/6388047V",
"cached": false
},
"notes": ["Status from the VAT registration only: the number is VAT-registered today (no register status)."]
}
vat.valid is null when VIES could not answer. vat.error then holds the code (MS_UNAVAILABLE, MS_MAX_CONCURRENT_REQ, TIMEOUT) and the top-level status is unknown: retry later. A sole trader or natural person answers not_company with nothing else, so no private name leaves the API.
With a national id instead of a VAT number, the register is asked live where it is open:
curl
curl -s "https://api.arcadiumdata.io/v1/companies/verify?id=923609016&country=NO"
Python
import time
import requests
def verify(vat: str):
r = requests.get("https://api.arcadiumdata.io/v1/companies/verify?vat=" + vat, timeout=60)
r.raise_for_status()
d = r.json()
if d["status"] == "unknown": # VIES or the register did not answer
return None
return d
for attempt in range(3):
result = verify("IE6388047V")
if result:
print(result["status"], result["statusBasis"], result["legalName"])
break
time.sleep(2 ** attempt)
TypeScript
type Verify = {
status: string;
statusBasis: string;
legalName: string | null;
vat: { valid: boolean | null; error: string | null } | null;
};
const res = await fetch('https://api.arcadiumdata.io/v1/companies/verify?vat=IE6388047V', {
headers: { Authorization: 'Bearer ' + process.env.ARCADIUM_API_KEY }, // optional: leave out for the anonymous tier
});
const body = (await res.json()) as Verify;
if (body.status === 'unknown') console.log('retry later', body.vat?.error);
else console.log(body.status, body.statusBasis, body.legalName);
To go from a VAT number to the company's website, registry ids and LEI, call GET /v1/companies/resolve with the number as id; the next guide covers it. The same lookups run without code in the company lookup and through the MCP server, so an AI agent can ask for a live check in plain language.
What we do not do: we do not search VIES by name (it cannot), we do not return officers or owners, and for Germany and Spain the legal name stays empty because VIES withholds it. A verify call is one outbound check, so it has its own, lower anonymous daily budget than the stored-data routes.
Comparison
| VIES direct | Arcadium Data /v1/companies/verify | |
|---|---|---|
| Key or account | None | None for the anonymous tier; free key for more |
| Retries and error codes | You write them | Returned as vat.error with status: unknown |
| Caching | None | 24 hours (cached) |
| Register status | Not included | Norway, Finland, France, Czech Republic |
| Sole traders | Name may be a private person | Answers not_company, nothing else |
| Name from Germany or Spain | --- | Empty |
| Great Britain | Not covered | Not a VAT check; see the Companies House guide in the backlog |
| Search by name, bulk file | No | No (name lookups: the resolve route) |
| Evidence for a tax authority | Consultation number route | Not a substitute; use VIES with your own VAT number |
Get the data without the plumbing
Arcadium Data serves these datasets from one REST API and one MCP server, with sources, licences and dates on every answer. No key is needed to try the routes in this guide.
Related
- Find a company VAT and registration number
- LEI lookup with the GLEIF API
- Company register APIs in Europe
- Company lookup
- MCP server
Sources
- VIES: VAT number validation
- VIES technical information (SOAP and REST)
- Commission Decision 2011/833/EU on the reuse of Commission documents
- HMRC: check a UK VAT number
Last updated 2026-10-25. This guide is ours and independent of the organisations it describes. Found a mistake? Tell us and we correct it.
Questions
Is there an official VIES API and does it need a key?
Yes. VIES has a SOAP service and a REST API at ec.europa.eu/taxation_customs/vies/rest-api. Neither needs an account or a key. Availability depends on the member state that owns the number.
Why does VIES say the service is unavailable for some numbers?
Each request is forwarded to the member state of the number. When that national system is down or busy, VIES returns MS_UNAVAILABLE, MS_MAX_CONCURRENT_REQ or a timeout. It is not a statement about the number: retry later with backoff.
Can I check a UK VAT number in VIES?
No. Great Britain left VIES after Brexit. Only Northern Ireland numbers (prefix XI, goods) are still covered. HMRC offers a separate service for GB numbers.
Does a valid VAT number mean the company is active?
No. It means the number is registered for VAT today. A company in liquidation can still hold a registration. Use the national register status as well; our verify route returns it for Norway, Finland, France and the Czech Republic.
Can I look up a VAT number by company name?
Not in VIES. For name to VAT lookups use a national register or the resolve route of our API (GET /v1/companies/resolve with name and country), which answers from stored register, legal-notice and GLEIF data.
Cite as: Arcadium Data, "EU VAT number check API: VIES REST, error codes and what it cannot tell you", https://arcadiumdata.io/guides/eu-vat-number-check-vies-api/, data as of 2026-10-25.