DEV Community

Fast Data
Fast Data

Posted on

How to validate EU VAT numbers in bulk (VIES) with Python or n8n, and the 4 gotchas nobody tells you

If you sell B2B inside the EU, you need to validate your customers' VAT numbers. A valid number lets you invoice without VAT under the reverse-charge rules; an invalid one means you should charge VAT. The official tool is VIES, the European Commission's VAT Information Exchange System. It is free and public, and its web form checks exactly one number at a time.

This guide shows how to check hundreds of numbers automatically, and the four things that will trip you up when you do.

Disclosure: I built the EU VAT Number Validator used in the examples. Everything it returns comes straight from the official VIES service; the gotchas below apply no matter which tool you use.

Option 1: call VIES yourself

VIES has a REST endpoint. A minimal Python check looks like this:

import httpx

def check_vat(country: str, number: str) -> dict:
    r = httpx.post(
        "https://ec.europa.eu/taxation_customs/vies/rest-api/check-vat-number",
        json={"countryCode": country, "vatNumber": number},
        timeout=30,
    )
    r.raise_for_status()
    return r.json()

print(check_vat("IE", "6388047V"))
Enter fullscreen mode Exit fullscreen mode

That works for a handful of numbers. For a real customer list you will hit the gotchas below within minutes.

The 4 gotchas

1. Member states go offline, and VIES tells you so

VIES does not hold the data itself. It forwards every request to the tax authority of the country in question. When that national system is down or busy, VIES answers with errors such as MS_UNAVAILABLE, TIMEOUT or SERVICE_UNAVAILABLE. This is not "invalid". It means "ask again later".

Fix: treat these as a separate status, retry with backoff (for example after 2, 4 and 8 seconds), and never mark a customer's number as invalid because a national server was busy.

2. Germany (and others) never return a name or address

For some countries VIES only answers "valid" or "invalid", and the name and address come back as ---. In my test, the VAT number of SAP SE (DE143454214) came back valid with no name and no address, while Google Ireland (IE6388047V) came back valid with GOOGLE IRELAND LIMITED and its Dublin address.

Fix: don't build your process around name matching for every country. Store null instead of ---.

3. Formats differ per country, so check them before calling VIES

Every EU country has its own VAT number format. DE is 9 digits, NL ends in B plus two digits, FR has two check characters up front, and Greece uses the prefix EL, not GR. Northern Ireland has its own prefix, XI. Customers type numbers with spaces, dots and dashes, or without a prefix at all.

Fix: normalise first (strip spaces, dots and dashes, uppercase, map GR to EL), then run a per-country format check. A number like DE12345 can be rejected locally, without spending a VIES call or waiting on a timeout.

4. "Valid" today is not proof forever unless you keep a consultation number

If you pass your own VAT number as the requester, VIES returns a request identifier (consultation number). It can serve as evidence that you checked the number on that date. Without it you only have your own log entry.

Fix: always send your requester VAT number and store requestIdentifier and requestDate with the customer record.

Option 2: do it in bulk with one call

The validator wraps all four fixes: normalisation, format pre-checks for all 27 EU countries plus XI, retries for unavailable member states, --- turned into null, and the consultation number when you pass yours:

curl -X POST "https://api.apify.com/v2/acts/fast_data_asap~eu-vat-number-validator/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "vatNumbers": ["IE6388047V", "DE 143 454 214", "DE12345"],
    "requesterVatNumber": "DE123456789"
  }'
Enter fullscreen mode Exit fullscreen mode

These were the real results of my test run (3.8 seconds):

Input Status Name Charged
IE6388047V valid GOOGLE IRELAND LIMITED yes
DE143454214 valid null (Germany shares no name) yes
DE12345 format_error – no

Each row also has countryCode, vatNumber, address, requestDate, requestIdentifier and an error text. It costs $0.001 per number VIES actually answered; format errors and unavailable member states are free.

Replace DE123456789 with your own VAT number; the placeholder will not give you a valid consultation number.

Automating it in n8n or Make

A common setup is a weekly re-check of all B2B customers:

  1. Schedule Trigger (weekly) → Google Sheets / CRM node reads customer VAT numbers.
  2. HTTP Request node: POST the list to the endpoint above, with your Apify token in a Header Auth credential (Authorization: Bearer …).
  3. IF node on status: invalid → create a task for accounting; service_unavailable → retry next run; valid → write requestIdentifier and date back to the sheet.

That turns a manual, one-by-one job into a weekly report that only lists the numbers someone actually needs to look at.

VIES results come from the European Commission's public service. This article is about the technical process, not tax advice. Ask your tax adviser how the reverse-charge rules apply to your business.

Top comments (0)