Vatline

How to validate an EU VAT number in Python

Every B2B sale inside the EU should check the customer's VAT number against VIES, the European Commission's validation service. Here is how to do it from Python, with no SOAP library.

1. Normalise and check the format

Strip spaces, dots and dashes, uppercase, and split the two-letter prefix. Greece uses EL in VIES, not GR; Northern Ireland uses XI.

import re

def split_vat(raw):
    s = re.sub(r"[\s.\-]", "", raw).upper()
    country, number = s[:2], s[2:]
    return ("EL" if country == "GR" else country), number

Then check the per-country structure (for example France: 2 check characters + 9-digit SIREN; Germany: 9 digits). The full list is on VAT number formats. Rejecting malformed input here saves a network call and a confusing fault.

2. Call VIES over SOAP

import requests

VIES = "https://ec.europa.eu/taxation_customs/vies/services/checkVatService"
ENVELOPE = """<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
 <soap:Body>
  <checkVat xmlns="urn:ec.europa.eu:taxud:vies:services:checkVat:types">
   <countryCode>{country}</countryCode>
   <vatNumber>{number}</vatNumber>
  </checkVat>
 </soap:Body>
</soap:Envelope>"""

def check_vat(country, number):
    r = requests.post(VIES, data=ENVELOPE.format(country=country, number=number),
                      headers={"Content-Type": "text/xml; charset=utf-8"},
                      timeout=10)
    body = r.text
    fault = re.search(r"<faultstring>(.*?)</faultstring>", body)
    if fault:
        raise RuntimeError(fault.group(1))   # e.g. MS_MAX_CONCURRENT_REQ
    tag = lambda t: (re.search(rf"<(?:\w+:)?{t}>(.*?)</(?:\w+:)?{t}>", body, re.S) or [None, None])[1]
    return {"valid": tag("valid") == "true", "name": tag("name"), "address": tag("address")}

3. Read the answer carefully

4. Keep proof of the check

For an audit trail, use the checkVatApprox operation with your own VAT number as requester: VIES then returns a requestIdentifier (the consultation number) you can store with the invoice.

The same thing in one REST call

If you would rather not maintain SOAP parsing, caching and retries yourself:

r = requests.post("https://vatline.p.rapidapi.com/v1/check",
                  json={"vat_number": "FR40303265045"},
                  headers={"X-RapidAPI-Key": KEY,
                           "X-RapidAPI-Host": "vatline.p.rapidapi.com"})
print(r.json())   # {"valid": true, "name": "...", "name_available": true, ...}

Skip the plumbing. Vatline wraps VIES in a REST/JSON API with caching, retry hints and per-country circuit breakers. Free plan: 300 checks a month, no card. Read the docs or try a number now.