vatverify home
All guides

How to validate VAT numbers in Python

Validate EU, UK, Swiss, and Norwegian VAT numbers from Python by calling the vatverify REST API directly with httpx or requests. Sync, async, and offline modes.

TL;DR

  • There is no official Python SDK yet (on the roadmap). For now, call the REST API directly with httpx or requests.
  • GET https://api.vatverify.dev/v1/validate?vat_number=IE6388047V with an Authorization: Bearer header.
  • For offline format + checksum validation, install vatverify-rates (pip install vatverify-rates).
Python package status

vatverify-rates ships on PyPI alongside the API launch (v0.1.0 packaged, 160 tests green). Reading this before launch? The REST API is live today. The httpx / requests examples below work without any package install. The JavaScript sibling @vatverify/vat-rates is already on npm if you need offline checksum validation today.

Why format checking isn't enough

A regex can confirm that DE123456789 looks like a German VAT number (two letters, nine digits, correct prefix). It cannot confirm the final check digit is mathematically correct, and it cannot confirm the business is actively registered.

Every EU country embeds a checksum algorithm into its VAT number format: Germany and the Netherlands use MOD-11 variants, France layers a MOD-97 key onto the SIREN, Italy uses a Luhn-style check, the UK uses HMRC's 97-55 algorithm, Norway and Switzerland run their own MOD-11 schemes. python-stdnum and similar libraries implement these, but format + checksum still only tells you a number is well-formed, not that it belongs to a live, VAT-registered business.

DE000000000 passes a format regex. It fails the German MOD-11 check. Even a number that passes both can be deregistered, recycled, or typoed into a different company's number. Only a live lookup against the national registry settles that, which is what the live validator does.

Calling the REST API from Python

The public API is plain REST with JSON responses. Any HTTP client works. Examples below use httpx (supports both sync and async) and the popular requests library.

Sync with httpx

validate.py
import os
import httpx

API_KEY = os.environ["VATVERIFY_API_KEY"]
BASE_URL = "https://api.vatverify.dev"


def validate_vat(vat_number: str) -> dict:
    response = httpx.get(
        f"{BASE_URL}/v1/validate",
        params={"vat_number": vat_number},
        headers={"Authorization": f"Bearer {API_KEY}"},
        timeout=30.0,
    )
    response.raise_for_status()
    return response.json()


result = validate_vat("IE6388047V")
data = result["data"]
if data["valid"]:
    company = data.get("company") or {}
    print(f"Valid: {company.get('name', data['vat_number'])}")
else:
    print(f"{data['vat_number']} is not registered.")

Sync with requests

validate_requests.py
import os
import requests

API_KEY = os.environ["VATVERIFY_API_KEY"]

response = requests.get(
    "https://api.vatverify.dev/v1/validate",
    params={"vat_number": "IE6388047V"},
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=30,
)
response.raise_for_status()
result = response.json()
print(result["data"]["valid"], result["data"]["country"]["code"])

Async with httpx.AsyncClient

Use this shape inside FastAPI, Starlette, or any async framework. Share one AsyncClient across the process to reuse the connection pool.

validate_async.py
import asyncio
import os
import httpx

API_KEY = os.environ["VATVERIFY_API_KEY"]

client = httpx.AsyncClient(
    base_url="https://api.vatverify.dev",
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=30.0,
)


async def validate_vat(vat_number: str) -> dict:
    response = await client.get("/v1/validate", params={"vat_number": vat_number})
    response.raise_for_status()
    return response.json()


async def main() -> None:
    result = await validate_vat("IE6388047V")
    print(result["data"]["valid"])


asyncio.run(main())

Handling rate limits (HTTP 429)

When you exceed your plan limit, the API returns 429 with a Retry-After header (in seconds). Honour it: it's the single source of truth for when you can retry.

rate_limits.py
import time
import httpx


def validate_with_retry(vat_number: str, client: httpx.Client) -> dict:
    for _ in range(3):
        response = client.get("/v1/validate", params={"vat_number": vat_number})
        if response.status_code == 429:
            wait = int(response.headers.get("Retry-After", "1"))
            time.sleep(wait)
            continue
        response.raise_for_status()
        return response.json()
    raise RuntimeError("rate limited after 3 attempts")

Offline format + checksum with vatverify-rates

For format + checksum validation without a network call, install the offline package. Zero runtime dependencies, works in lambdas and containers without any credentials:

pip install vatverify-rates
offline.py
from vatverify_rates import validate, get_rate, get_standard_rate

result = validate("IE6388047V")
# ValidateResult(valid=True)

bad = validate("DE000000000")
# ValidateResult(valid=False, errors=("invalid checksum",))

rate = get_rate("DE")
# Rate(standard=19.0, reduced=(7.0,), super_reduced=None, ...)

print(get_standard_rate("DE"))  # 19.0

ValidateResult has two shapes: valid=True (no errors) or valid=False with an errors tuple. No API key, no network, no latency.

Hybrid pattern: offline filter then live lookup

The efficient pattern is to reject obvious garbage offline (zero cost), then call the API only for numbers that pass format + checksum:

hybrid.py
import os
import httpx
from vatverify_rates import validate as offline_validate

API_KEY = os.environ["VATVERIFY_API_KEY"]

client = httpx.Client(
    base_url="https://api.vatverify.dev",
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=30.0,
)


def validate_vat(vat_number: str) -> dict:
    offline = offline_validate(vat_number)
    if not offline.valid:
        return {"valid": False, "source": "offline", "errors": list(offline.errors)}

    response = client.get("/v1/validate", params={"vat_number": vat_number})
    response.raise_for_status()
    return {"source": "live", **response.json()}

Django and FastAPI patterns

For framework-specific guides, see the dedicated pages: Validate VAT in Django for forms, admin actions, DRF serializers, and async views.

For FastAPI, wrap the httpx.AsyncClient in a dependency:

dependencies.py
from fastapi import Depends, HTTPException, Query
import httpx

_client = httpx.AsyncClient(
    base_url="https://api.vatverify.dev",
    timeout=30.0,
)


async def require_valid_vat(vat_number: str = Query(...)) -> dict:
    import os
    response = await _client.get(
        "/v1/validate",
        params={"vat_number": vat_number},
        headers={"Authorization": f"Bearer {os.environ['VATVERIFY_API_KEY']}"},
    )
    if response.status_code == 429:
        raise HTTPException(status_code=429, detail="rate limited")
    response.raise_for_status()
    data = response.json()["data"]
    if not data["valid"]:
        raise HTTPException(status_code=422, detail=f"{data['vat_number']} is not registered")
    return data

Testing with test-mode keys

Use vtv_test_* keys for deterministic fixture responses. See Test mode.

tests/test_vat.py
import httpx


def test_valid_irish_vat_returns_company_name():
    response = httpx.get(
        "https://api.vatverify.dev/v1/validate",
        params={"vat_number": "IE6388047V"},
        headers={"Authorization": "Bearer vtv_test_xxx"},
    )
    response.raise_for_status()
    data = response.json()["data"]
    assert data["valid"] is True
    assert data["country"]["code"] == "IE"

FAQ

Is there an official Python SDK?

Not yet. A first-class Python SDK is on the roadmap. In the meantime, httpx or requests against the REST API is the supported integration path.

Can I use vatverify-rates offline in a lambda or container?

Yes, vatverify-rates has zero runtime dependencies and bundles its own country data. The installed wheel is small and works fine on cold-start-sensitive runtimes.

How does VIES downtime affect my integration?

When VIES is unavailable, the API returns the most recent cached result with meta.source_status: "degraded". See handle-vies-downtime for retry guidance.

What does an invalid VAT number response look like?

When the registry responds but the number is not registered, data.valid is false and data.company is null. Obvious format errors return a structured 400 response with an error.code like invalid_format or country_unsupported.

Validate VAT in three lines.

Free up to 500 requests per month. No credit card.

Start free