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.
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.
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")}
valid false is a normal answer, not an error.--- or nothing, so a missing name does not mean the call failed. See why VIES returns no company name.MS_MAX_CONCURRENT_REQ means the country is saturated: retry later, not immediately. See handling MS_MAX_CONCURRENT_REQ.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.
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.