Validate EU VAT in Python
Validate EU VAT in Python
To validate EU VAT numbers in Python, install the eurovalidate package and call validate_vat() with any European VAT number. The API checks the number against VIES (the EU's official VAT validation system), returns the company name and address, and caches results for 24 hours so repeat lookups resolve in 1--5 milliseconds. Here is how to get started.
Install
pip install eurovalidate
Sync example
from eurovalidate import EuroValidate
client = EuroValidate("ev_live_your_key_here")
result = client.validate_vat("NL820646660B01")
print(result.valid) # True
print(result.company_name) # "COOLBLUE B.V."
print(result.company_address) # "Weena 664 3012CN ROTTERDAM"
The VatResult dataclass gives you typed access to valid, vat_number, country_code, company_name, company_address, status, meta, and request_id.
Async example
For FastAPI, Django async views, or any asyncio application:
from eurovalidate import AsyncEuroValidate
async def check_vat():
async with AsyncEuroValidate("ev_live_your_key_here") as client:
result = await client.validate_vat("FR40303265045")
print(result.valid) # True
print(result.company_name) # "SOCIETE GENERALE"
The AsyncEuroValidate client uses httpx under the hood and manages its own connection pool. Always use it as a context manager to ensure connections are closed cleanly.
Using curl (no SDK)
If you want to test before writing any Python:
curl -H "X-API-Key: ev_live_your_key_here" \
https://api.eurovalidate.com/v1/vat/NL820646660B01
Using requests (no SDK)
If you prefer requests over a dedicated SDK:
import requests
response = requests.get(
"https://api.eurovalidate.com/v1/vat/NL820646660B01",
headers={"X-API-Key": "ev_live_your_key_here"},
timeout=10,
)
data = response.json()
print(data["status"]) # "valid"
print(data["company_name"]) # "COOLBLUE B.V."
This works but you lose typed responses, automatic retry on 429, and error mapping. The SDK is recommended for production use.
What the API returns
Valid VAT number
Request: GET /v1/vat/NL820646660B01
{
"vat_number": "NL820646660B01",
"country_code": "NL",
"status": "valid",
"company_name": "COOLBLUE B.V.",
"company_address": "Weena 664 3012CN ROTTERDAM",
"request_id": "req_abc123",
"meta": {
"confidence": "high",
"source": "vies_live",
"cached": false,
"response_time_ms": 247,
"last_verified": "2026-04-05T10:15:30Z",
"upstream_status": "ok"
}
}
Invalid VAT number
Request: GET /v1/vat/NL000000000B00
{
"vat_number": "NL000000000B00",
"country_code": "NL",
"status": "invalid",
"company_name": null,
"company_address": null,
"request_id": "req_def456",
"meta": {
"confidence": "high",
"source": "vies_live",
"cached": false,
"response_time_ms": 189,
"last_verified": "2026-04-05T10:15:35Z",
"upstream_status": "ok"
}
}
The status field is one of: valid, invalid, error, or unavailable. Check meta.confidence to understand data freshness: high means a live check against VIES, medium means cached, low means the upstream was down and the response is stale.
Timeout handling
VIES live lookups typically take 150--300 ms, but individual country backends can be slow or unresponsive. Always set a timeout:
from eurovalidate import EuroValidate, EuroValidateError
client = EuroValidate("ev_live_your_key_here")
try:
result = client.validate_vat("NL820646660B01")
if result.valid:
print(f"{result.company_name} at {result.company_address}")
else:
print(f"VAT number {result.vat_number} is not valid")
except EuroValidateError as e:
print(f"Validation failed: {e}")
The SDK sets a default timeout of 30 seconds and retries automatically on 429 (rate limit) responses. If VIES is down for a specific country, EuroValidate returns the last cached result with confidence: "low" rather than failing outright.
Caching strategy
EuroValidate operates a dual-layer cache:
- Redis (hot cache) -- sub-5ms reads for recently validated numbers.
- PostgreSQL (persistent cache) -- survives Redis restarts, stores results for the full 24-hour TTL.
For your own application, you generally do not need to add another caching layer. The API already caches aggressively. However, if you are validating the same numbers in a tight loop (for example, re-rendering a checkout page), store the result locally for the duration of the user session:
from functools import lru_cache
from eurovalidate import EuroValidate
client = EuroValidate("ev_live_your_key_here")
@lru_cache(maxsize=256)
def check_vat(vat_number: str) -> bool:
result = client.validate_vat(vat_number)
return result.valid
This avoids redundant API calls during a single user interaction without introducing stale data risk, since the cache is scoped to the process lifetime.
Latency
| Scenario | Latency |
| Cached result (Redis hit) | 1--5 ms |
| Live VIES lookup | 150--300 ms |
| VIES unavailable, stale cache | 2--8 ms |
The first validation for a given VAT number hits VIES. Every subsequent call within 24 hours is served from cache.
Common pitfalls
Germany and Spain return no company name
German (DE) and Spanish (ES) tax authorities do not disclose trader names or addresses through VIES. This is a deliberate data protection policy. When you validate a DE or ES VAT number, company_name and company_address will be null even for valid numbers.
If you need the company name for a German VAT number, EuroValidate cross-references GLEIF data when the company holds a Legal Entity Identifier (LEI). This is not guaranteed coverage, but it fills the gap for many large enterprises.
Greece: GR vs EL
VIES uses EL as the country prefix for Greece, while ISO 3166 uses GR. The API accepts both formats. The country_code field in the response always returns GR (ISO standard).
Rate limits
The free tier allows 100 requests per hour. Rate limit headers are included in every response:
X-RateLimit-Limit-- your plan's limitX-RateLimit-Remaining-- calls left in the current windowX-RateLimit-Reset-- Unix timestamp when the window resets
The SDK reads these headers and retries automatically on 429. If you are using requests directly, check the Retry-After header.
VIES downtime does not mean failure
VIES has roughly 70% effective uptime across all country backends. EuroValidate uses per-country circuit breakers -- 27 independent breakers, one per EU member state. If Germany is down, France still works. When a country backend is unavailable, the API returns the last known result with confidence: "low" and upstream_status indicating the outage. Your code should check the confidence level and decide whether stale data is acceptable for your use case.
Unified validation
Validate a VAT number and an IBAN in a single API call:
import requests
response = requests.post(
"https://api.eurovalidate.com/v1/validate",
headers={
"X-API-Key": "ev_live_your_key_here",
"Content-Type": "application/json",
},
json={
"vat_number": "FR40303265045",
"iban": "DE89370400440532013000",
},
timeout=10,
)
data = response.json()
print(data["vat"]["status"]) # "valid"
print(data["iban"]["valid"]) # True
The POST /v1/validate endpoint accepts vat_number, iban, and eori_number in any combination. One request, one API call on your quota.
Next steps
- Get your free API key -- no credit card, takes 30 seconds.
- Interactive API docs -- try every endpoint from your browser.
- Batch validation -- validate up to 1,000 VAT numbers asynchronously.
- IBAN validation endpoint -- validate IBANs with bank name and BIC lookup.
EuroValidate is a unified REST API for EU business data validation. It replaces VIES, EORI, IBAN, and GLEIF lookups with a single JSON endpoint. 27 EU countries plus the UK, per-country circuit breakers, dual-layer caching, and confidence scoring on every response.
