EU VAT Number Validation in Python
A complete tutorial for validating EU VAT numbers via VIES in Python. Covers the basics with httpx and requests, plus real-world examples for Django and FastAPI.
Why validate VAT numbers?
For EU B2B transactions, validating the customer's VAT number in VIES is a legal requirement before applying the reverse charge mechanism. Without a confirmed valid VAT number, you must charge local VAT. An invalid number on a zero-VAT invoice is a compliance liability.
The official VIES API is SOAP-based and requires zeep or similar XML libraries. vatnode wraps VIES in a REST interface — pure JSON, no SOAP client needed.
Basic validation with httpx
import os
import httpx
VATNODE_API_KEY = os.environ["VATNODE_API_KEY"]
def validate_vat(vat_id: str) -> dict:
"""Validate an EU VAT number via vatnode / VIES."""
response = httpx.get(
f"https://api.vatnode.dev/v1/vat/{vat_id}",
headers={"Authorization": f"Bearer {VATNODE_API_KEY}"},
timeout=10.0,
)
response.raise_for_status()
return response.json()
result = validate_vat("IE6388047V")
print(result["valid"]) # True
print(result["companyName"]) # GOOGLE IRELAND LIMITED
print(result["checkId"]) # store for invoice audit trail
print(result["countryVat"]["standardRate"]) # 23Using requests instead
import os
import requests
VATNODE_API_KEY = os.environ["VATNODE_API_KEY"]
def validate_vat(vat_id: str) -> dict:
response = requests.get(
f"https://api.vatnode.dev/v1/vat/{vat_id}",
headers={"Authorization": f"Bearer {VATNODE_API_KEY}"},
timeout=10,
)
response.raise_for_status()
return response.json()Handle VIES errors
VIES national nodes go offline. Handle VIES_UNAVAILABLE by queuing for retry — never block the transaction:
import httpx
from dataclasses import dataclass
from typing import Optional
@dataclass
class VatCheckResult:
valid: Optional[bool] # None = VIES unavailable, retry later
check_id: Optional[str]
company_name: Optional[str]
standard_rate: Optional[float]
error: Optional[str] = None
def validate_vat_safe(vat_id: str) -> VatCheckResult:
try:
response = httpx.get(
f"https://api.vatnode.dev/v1/vat/{vat_id}",
headers={"Authorization": f"Bearer {VATNODE_API_KEY}"},
timeout=10.0,
)
if response.status_code == 503:
return VatCheckResult(valid=None, check_id=None,
company_name=None, standard_rate=None,
error="VIES_UNAVAILABLE")
if response.status_code == 400:
return VatCheckResult(valid=False, check_id=None,
company_name=None, standard_rate=None,
error="INVALID_FORMAT")
data = response.json()
return VatCheckResult(
valid=data["valid"],
check_id=data["checkId"],
company_name=data.get("companyName"),
standard_rate=data["countryVat"]["standardRate"],
)
except httpx.TimeoutException:
return VatCheckResult(valid=None, check_id=None,
company_name=None, standard_rate=None,
error="TIMEOUT")FastAPI integration
Use a FastAPI dependency to validate VAT numbers at the endpoint level:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import httpx
import os
app = FastAPI()
VATNODE_API_KEY = os.environ["VATNODE_API_KEY"]
class CheckoutRequest(BaseModel):
vat_id: str
amount: float
@app.post("/checkout")
async def checkout(body: CheckoutRequest):
async with httpx.AsyncClient() as client:
res = await client.get(
f"https://api.vatnode.dev/v1/vat/{body.vat_id}",
headers={"Authorization": f"Bearer {VATNODE_API_KEY}"},
timeout=10.0,
)
if res.status_code == 503:
# VIES down — proceed without reverse charge
return {"vatRate": 0.23, "reverseCharge": False, "vatCheckId": None}
if not res.is_success:
raise HTTPException(status_code=400, detail="Invalid VAT number")
data = res.json()
return {
"vatRate": 0 if data["valid"] else 0.23,
"reverseCharge": data["valid"],
"vatCheckId": data["checkId"],
"companyName": data.get("companyName"),
}Django integration
# views.py
import json
import httpx
from django.conf import settings
from django.http import JsonResponse
from django.views.decorators.http import require_POST
@require_POST
def validate_vat_view(request):
body = json.loads(request.body)
vat_id = body.get("vatId", "").strip()
if not vat_id:
return JsonResponse({"error": "vatId required"}, status=400)
try:
res = httpx.get(
f"https://api.vatnode.dev/v1/vat/{vat_id}",
headers={"Authorization": f"Bearer {settings.VATNODE_API_KEY}"},
timeout=10.0,
)
if res.status_code == 503:
return JsonResponse({"status": "retry_later"})
data = res.json()
return JsonResponse({
"valid": data["valid"],
"companyName": data.get("companyName"),
"checkId": data["checkId"],
})
except httpx.TimeoutException:
return JsonResponse({"status": "retry_later"})Related guides
Add VAT validation to your Python app
Free plan: 20 requests/month, no credit card. Works with any HTTP library.