EU VIES checks VAT numbers of the 27 member states and Northern Ireland. It does not check Swiss numbers. So the CHE column in your supplier sheet needs another route.
That route is free. The Swiss Federal Statistical Office (BFS) runs the UID register with a public web service. This tutorial shows the number format, the check digit and the SOAP call, in Python you can run today.
Why VIES rejects Swiss numbers
VIES is a European Commission search engine. It reads national VAT databases to show if a business can trade cross-border within the EU. Switzerland is not a member state, and Swiss VAT is a separate tax on transactions on Swiss territory.
VIES shows the gap itself. Its configuration endpoint lists 27 member state codes plus XI for Northern Ireland, and no CH. On 6 November 2026, its check endpoint answered INVALID_INPUT to the country code CH.
A UID and a VAT number
The UID identifies a Swiss business. It is CHE plus nine digits, assigned at random. The digits say nothing about the company.
The VAT number is the UID plus a suffix: CHE-123.456.789 MWST. The suffix can also be TVA or IVA. The Federal Tax Administration does not allow VAT as a suffix, but SECO's English page prints it. Accept it on input, because your data may contain it.
The check digit in 10 lines
The rule stands in eCH-0097, section 2.4.2:
- Multiply the first 8 digits by 5, 4, 3, 2, 7, 6, 5, 4.
- Divide the sum by 11 and subtract the remainder from 11.
- A remainder of 0 gives the check digit 0.
- A result of 10 means nobody gets this number.
eCH marks that version as repealed, so we tested the rule against the live register. For CHE-123.456.789, GetByUID answered "Invalid UID. The checksum is invalid".
import re
import time
import urllib.error
import urllib.request
import xml.etree.ElementTree as ET
WEIGHTS = (5, 4, 3, 2, 7, 6, 5, 4)
PATTERN = re.compile(
r"^CHE[-.\s]*(\d{3})[-.\s]*(\d{3})[-.\s]*(\d{3})(?:\s*(?:MWST|TVA|IVA|VAT))?$",
re.IGNORECASE,
)
def check_digit(first8: str) -> int | None:
remainder = sum(int(d) * w for d, w in zip(first8, WEIGHTS)) % 11
if remainder == 0:
return 0
digit = 11 - remainder
return None if digit == 10 else digit
def normalise(raw: str) -> str | None:
"""Return the nine digits, or None for a bad format or check digit."""
match = PATTERN.match(raw.strip())
if not match:
return None
digits = "".join(match.groups())
return digits if check_digit(digits[:8]) == int(digits[8]) else None
print(check_digit("10275393"), check_digit("12345678"), check_digit("10000016"))
print(normalise("CHE-102.753.938 MWST"), normalise("CHE-123.456.789"))
Output of a run on 6 November 2026:
8 8 None
102753938 None
CHE-102.753.938 is Swisscom AG, taken from the public register. Its last digit is 8, so it passes.
CHE-123.456.789 is the fictional number in the official format examples. Its check digit should be 8, so it fails. Do not use it as a test fixture for a valid number.
10000016 is a fictional prefix that gives the result 10.
Ask the register
A correct check digit shows only that the number is well formed. The public service tells you the rest. It is free, needs no registration and allows 20 requests a minute. Above that, it blocks the caller for a while.
The interface specification (German, section 4) separates three calls:
-
ValidateVatNumberreturns true only for an active VAT number. -
ValidateUIDreturns true for an assigned UID, also when the company no longer exists. -
GetByUIDreturns the public entry.
For a VAT check, use the first one.
URL = "https://www.uid-wse.admin.ch/V5.0/PublicServices.svc"
NS = "http://www.uid.admin.ch/xmlns/uid-wse"
E97 = "http://www.ech.ch/xmlns/eCH-0097/5"
E108 = "http://www.ech.ch/xmlns/eCH-0108/5"
def call(method: str, body: str) -> ET.Element:
envelope = (
'<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">'
f'<s:Body><{method} xmlns="{NS}">{body}</{method}></s:Body></s:Envelope>'
)
headers = {
"Content-Type": "text/xml; charset=utf-8",
"SOAPAction": f'"{NS}/IPublicServices/{method}"',
}
request = urllib.request.Request(URL, envelope.encode(), headers)
with urllib.request.urlopen(request, timeout=30) as response:
return ET.fromstring(response.read())
def vat_is_active(digits: str) -> bool:
root = call("ValidateVatNumber", f"<vatNumber>CHE{digits}</vatNumber>")
return root.findtext(f".//{{{NS}}}ValidateVatNumberResult") == "true"
def check_all(numbers: list[str]) -> dict[str, str]:
results = {}
for raw in numbers:
digits = normalise(raw)
if digits is None:
results[raw] = "rejected locally"
continue
try:
results[raw] = "active" if vat_is_active(digits) else "not active"
except urllib.error.URLError as err:
results[raw] = f"not checked: {err}"
time.sleep(3.2) # stays under 20 requests a minute
return results
def get_by_uid(digits: str) -> ET.Element:
return call(
"GetByUID",
f'<uid><uidOrganisationIdCategorie xmlns="{E97}">CHE</uidOrganisationIdCategorie>'
f'<uidOrganisationId xmlns="{E97}">{digits}</uidOrganisationId></uid>',
)
numbers = ["CHE-102.753.938 MWST", "CHE-123.456.789", "CHE-123.456.788", "not a number"]
for number, result in check_all(numbers).items():
print(f"{number:22} {result}")
entry = get_by_uid("102753938")
print(entry.findtext(f".//{{{E97}}}organisationLegalName"))
print(entry.findtext(f".//{{{E108}}}vatEntryDate"))
Output of a run on 6 November 2026:
CHE-102.753.938 MWST active
CHE-123.456.789 rejected locally
CHE-123.456.788 not active
not a number rejected locally
Swisscom AG
1998-01-01
CHE-123.456.788 is the same fictional number with a correct check digit. The register held no entry for it on that day. A false answer does not say why a number is not active, so GetByUID is your next step. A refused call came back as HTTP 500 with a SOAP fault, and its errorDetail holds the reason.
What the register leaves to you
-
Speed. At 3.2 seconds a call, 1,000 numbers take about 53 minutes. The specification names the fault
Request_limit_exceeded. - Batch work. BFS offers a batch query for organisations with a legitimate interest. The first working hour is free. Each further hour costs CHF 130 excluding VAT.
-
Personal data. BFS publishes name and address as core data. The legal forms include the sole proprietorship (
0101), so an entry can name a person. Decide what you store before you store it. - Change. The Business Enterprise Register is to replace the UID register from 2028. Keep the service URL in your configuration.
This article was created with the help of AI. A run of the code produced the output shown, and the links are the sources for its facts.
Top comments (0)