Bosco Labs

Blog › Validate EU VAT numbers with the VIES API in Python

Validate EU VAT numbers with the VIES API in Python

Bosco Labs ·

Send a POST to the European Commission's VIES REST endpoint with the country code and the number. No key, no account. You get back valid, and for most countries the registered name and address. When a member state's system is down you get an error, and you should treat that as "unknown" and try again later. Never record it as invalid.

curl -s -X POST \
  https://ec.europa.eu/taxation_customs/vies/rest-api/check-vat-number \
  -H 'Content-Type: application/json' \
  -d '{"countryCode": "IE", "vatNumber": "6388047V"}'

The response has valid (a boolean), name, address, requestDate and a few more fields. The Commission publishes the full schema as a Swagger file, and it's worth a look because the field names differ from the old SOAP service that a lot of libraries still wrap.

A small Python function

Most of the VIES libraries on PyPI were written for the SOAP interface, and some haven't been touched in years. The REST call is simple enough that you don't need one.

import re
import requests

VIES = "https://ec.europa.eu/taxation_customs/vies/rest-api/check-vat-number"


def check_vat(raw: str) -> dict:
    vat = re.sub(r"[\s.\-]", "", raw).upper()
    country, number = vat[:2], vat[2:]
    if country == "GR":
        country = "EL"  # VIES uses EL for Greece

    try:
        r = requests.post(
            VIES,
            json={"countryCode": country, "vatNumber": number},
            timeout=20,
        )
        data = r.json()
    except (requests.RequestException, ValueError) as exc:
        return {"vat": vat, "status": "unknown", "error": str(exc)}

    if "valid" not in data:
        errors = [e.get("error") for e in data.get("errorWrappers", [])]
        return {"vat": vat, "status": "unknown", "error": errors}

    def shared(field):  # VIES sends "---" when the member state doesn't share it
        value = data.get(field)
        return None if value in (None, "", "---") else value

    return {
        "vat": vat,
        "status": "valid" if data["valid"] else "invalid",
        "name": shared("name"),
        "address": shared("address"),
        "checked_at": data.get("requestDate"),
    }


print(check_vat("IE 6388047V"))

Keep unknown as its own outcome in your data model, next to valid and invalid. If you collapse unknown into invalid, you'll reject good suppliers every time a national system has a bad morning.

Things that will bite you

Greece is EL in VIES, even though its ISO code is GR. People type GR all the time, so map it.

UK numbers aren't in VIES any more. Since Brexit, a GB number has to be checked with HMRC's own VAT API. Northern Ireland businesses trading goods with the EU have XI numbers, and those are in VIES.

Some member states don't share the name and address. Germany is the one you'll notice first: the number comes back valid with --- as the name and address. That means you can't confirm the number belongs to the company that sent you the invoice, only that it's a live number. If name matching matters to you, the VIES request also accepts the trader's name and address, and some countries answer with a match result per field.

VIES is a front for the national systems, so it's only as available as they are. Errors such as MS_UNAVAILABLE (the member state isn't answering) and MS_MAX_CONCURRENT_REQ (too many requests at once) are normal. Retry with a backoff, don't fire hundreds of requests in parallel, and cache answers for a while. A VAT registration doesn't change by the minute.

Some countries also throttle identical repeated requests, so don't re-check the same number in a loop to "make sure".

If you need proof that you checked, pass your own VAT number as requesterMemberStateCode and requesterNumber. The response then includes a requestIdentifier, a consultation number you can store with the invoice. Tax auditors in some countries ask for exactly that.

What "valid" proves

A valid result means the number is registered for intra-EU trade right now, in that country. That's useful, and it's also less than people assume.

It doesn't prove the company sending you the invoice is the company the number belongs to. Invoice fraud usually involves a real company's details with someone else's bank account. For that you need to match the VIES name against the company you expect, and you need to check the bank account separately.

IBAN validation is offline arithmetic. It checks the country's length and layout and the ISO 13616 check digits, and it can read out the bank code. It doesn't tell you who holds the account. Verification of Payee is what ties a name to an account, and banks in the euro area have had to offer it for euro credit transfers since October 2025. If a supplier changes their bank details, call them on a number you already had.

The hosted version

Our VAT and IBAN check wraps all of this: VIES for VAT numbers, offline IBAN checks and a name match against the company you expect. Unknown answers come back as unverified with a reason, and they cost nothing.

import os
import httpx

r = httpx.post(
    "https://api.boscolabs.dev/v1/vat-iban-check",
    headers={"Authorization": f"Bearer {os.environ['BOSCO_API_KEY']}"},
    json={
        "vatNumbers": ["IE6388047V"],
        "ibans": ["NL91ABNA0417164300"],
        "expectedName": "Google Ireland",
        "expectedCountry": "IE",
    },
    timeout=60,
)
r.raise_for_status()

for row in r.json()["rows"]:
    print(row["check_type"], row["number"], row["status"], row["reason"])
    for c in row["checks"]:
        print("   ", c["check"], c["status"], c["detail"])

In that example the VAT number passes and matches the name, and the IBAN is well formed but Dutch, so country_match fails against the expected Ireland. That's the kind of thing you want a person to look at before paying. It costs $0.002 per definite answer, and malformed or unverified numbers are free. A key comes with $5 of free credit.

If you're onboarding a supplier, the VAT number is one check out of several. Our counterparty-check takes the company number, the VAT number and the IBAN together, then adds register status, insolvency and sanctions screening. That's $0.05 per company. There's a benchmark of it against web search if you want to see where it helps and where it doesn't.

Bosco Labs makes the VAT and IBAN check and counterparty-check.

Bosco Labs: official-source data for AI agents, as an HTTP API, MCP server and Apify Actors. API docs.