How to Validate EU VAT Numbers in Python
Why VAT validation matters
If you sell to businesses across the EU, you need to verify their VAT numbers. A valid VAT number determines whether you charge VAT or apply the reverse charge mechanism. Getting it wrong means either overcharging your customer or underreporting to tax authorities.
The official way to validate is through VIES (VAT Information Exchange System), operated by the European Commission. But VIES has reliability issues — timeouts, downtime, and inconsistent responses. That's where an API wrapper helps.
The quick way: European Business API
The fastest approach is a single API call:
import httpx
response = httpx.get(
"https://frenchbusinessapi.com/vat/validate/FR89421940101",
headers={"X-API-Key": "your_api_key"}
)
data = response.json()
print(data)
Response:
{
"valid": true,
"country_code": "FR",
"vat_number": "FR89421940101",
"format_valid": true,
"company_name": "GOOGLE FRANCE",
"company_address": "8 RUE DE LONDRES 75009 PARIS",
"vies_available": true
}
One call gives you: format validation, VIES live check, company name and address. No need to parse SOAP XML yourself.
How VAT numbers work
Every EU VAT number starts with a two-letter country prefix followed by digits (and sometimes letters). The format varies by country:
- France: FR + 2 key digits + 9 SIREN digits (e.g., FR89421940101)
- Germany: DE + 9 digits (e.g., DE123456789)
- Netherlands: NL + 9 digits + B + 2 digits (e.g., NL123456789B01)
- Ireland: IE + 7 digits + 1-2 letters (e.g., IE1234567A)
Each country has its own format rules. A proper validation needs to check both the format AND the live status via VIES.
Format validation vs VIES validation
There are two levels of validation:
Format validation checks if the number matches the expected pattern for its country. This is fast and offline — you can do it with a regex. But a correctly formatted number can still be invalid (expired, cancelled, never issued).
VIES validation checks the number against the actual EU database. This confirms the number is currently active and returns the registered company name and address. It requires a network call and can be slow or unavailable.
A robust implementation does both: format check first (fast rejection of obviously wrong numbers), then VIES check for format-valid numbers.
Checking reverse charge eligibility
When you sell B2B across EU borders, the reverse charge mechanism applies: you invoice without VAT, and the buyer self-assesses VAT in their country. But this only applies when specific conditions are met:
- Seller and buyer are in different EU countries
- The buyer is a VAT-registered business
- The transaction is B2B (not B2C)
The API handles this logic:
response = httpx.get(
"https://frenchbusinessapi.com/vat/check-reverse-charge",
params={
"seller_country": "FR",
"buyer_country": "DE",
"buyer_is_business": True
},
headers={"X-API-Key": "your_api_key"}
)
print(response.json())
Response:
{
"reverse_charge_applies": true,
"vat_rate_to_apply": 0.0,
"explanation": "Intra-EU B2B supply from FR to DE. Reverse charge applies.",
"seller_country": "FR",
"buyer_country": "DE",
"buyer_is_business": true
}
Getting VAT rates for all 27 EU countries
Need the current VAT rates? The API provides standard, reduced, super-reduced, and parking rates for all 27 EU member states:
response = httpx.get(
"https://frenchbusinessapi.com/vat/rates",
headers={"X-API-Key": "your_api_key"}
)
for country in response.json():
print(f"{country['country_code']}: {country['standard_rate']}%")
This data is updated when EU countries change their rates. No need to maintain a hardcoded table.
Handling VIES downtime
VIES is not always available. Individual country services go down regularly. The API response includes a `vies_available` field that tells you whether the live check was possible:
data = response.json()
if data["vies_available"]:
# VIES confirmed the result
is_valid = data["valid"]
else:
# VIES was down, fall back to format check only
is_valid = data["format_valid"]
# Consider retrying later for a definitive answer
This lets you handle degraded mode gracefully instead of failing hard.
Batch validation
If you need to validate multiple VAT numbers (e.g., cleaning up a customer database), loop through them with reasonable rate limiting:
import time
vat_numbers = ["FR89421940101", "DE123456789", "NL123456789B01"]
for vat in vat_numbers:
response = httpx.get(
f"https://frenchbusinessapi.com/vat/validate/{vat}",
headers={"X-API-Key": "your_api_key"}
)
result = response.json()
status = "valid" if result["valid"] else "invalid"
print(f"{vat}: {status} — {result.get('company_name', 'N/A')}")
time.sleep(0.1) # Respect rate limits
Common pitfalls
- Don't strip the country prefix: the API expects the full VAT number including the country code
- Greece uses EL, not GR: Greek VAT numbers start with EL (e.g., EL123456789), not the ISO country code GR
- Spaces and dashes are OK: the API strips them automatically, so "FR 89 421 940 101" works
- VIES downtime is normal: always handle the `vies_available: false` case
Next steps
- Get your free API key — 100 requests/day, no credit card
- API documentation — all endpoints, parameters, and response formats
- Check out our guides on IBAN validation, SEPA XML generation, and French company lookup
Ready to build for Europe?
One API for company lookup, VAT validation, IBAN verification, SEPA payments, and more.
Get your API key