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

Python — 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"])  # 23

Using requests instead

Python — requests
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:

Python — error handling
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:

Python — FastAPI
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

Python — Django view
# 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"})

Add VAT validation to your Python app

Free plan: 20 requests/month, no credit card. Works with any HTTP library.