EU VAT number validation: a VIES timeout is not “invalid”
VIES is free and drops member states one at a time. Treating a timeout as invalid charges VAT to a B2B buyer who should have been reverse-charged.
Intra-EU B2B invoices hang on one question: is this counterparty’s VAT number registered in the member state that issued it? If yes, you reverse-charge and charge 0% VAT. If no, you add your domestic rate. Get that binary wrong in either direction and you either under-collect tax or overcharge a customer who then disputes the invoice. The official register is VIES. VIES is free. VIES is also the unreliable part of the job.
What actually fails in production
VIES is not one service. Each member state runs its own lookup; the Commission’s REST façade
(ec.europa.eu/taxation_customs/vies/rest-api) fans out. Germany being up says nothing about
Italy. A 503, a 12-second hang, or an empty body is a transport failure. A false from
a healthy member-state service is a registration failure. Those two must not share a status code
in your billing engine, because they have opposite commercial meanings:
- Invalid (format wrong, or VIES said “not registered”) → charge VAT. Safe default when you know the number is junk.
- Unknown (VIES did not answer) → do not treat as invalid. Retry, queue, or sell under a hold. Inventing “invalid” here means adding 19–27% to an invoice that should have been reverse-charged.
valid: false. That is the expensive bug. The number’s format can be perfect and
the company real; you just asked Italy during a window where Italy’s VIES node was down.Check format locally before you spend an upstream call
A German VAT number is DE + 9 digits. Ireland has three historic shapes. Romania is 2–10
digits. Prefixing GB is not a typo you can “fix” into VIES: Great Britain left; Northern
Ireland businesses use XI. Switzerland, Norway and Türkiye are not in the EU VAT area at
all — looking them up on VIES is a category error, not a failed checksum.
function parseEuVat(raw) {
const s = String(raw).replace(/[\s.\-_/]/g, '').toUpperCase();
const m = /^([A-Z]{2})(.+)$/.exec(s);
if (!m) return { ok: false, reason: 'missing country prefix' };
const [, code, number] = m;
// DE: 9 digits. Do not call VIES if this fails.
if (code === 'DE' && !/^\d{9}$/.test(number)) {
return { ok: false, reason: 'German VAT numbers are DE + 9 digits' };
}
return { ok: true, code, number };
}
Format failure is the one case you can call invalid without talking to Brussels. Everything past this point needs the register — and a third state for when the register is silent.
The catalogue call
The endpoint talks to VIES, caches the answer for 24 hours, and reports whether that member state’s
node currently claims to be up. It never returns invalid because the HTTP call failed.
curl -s https://api.temsor.com/v1/eu/vat/validate \
-H 'content-type: application/json' \
-d '{"vatNumber":"DE811907980"}'
{
"verdict": "valid",
"formatValid": true,
"countryCode": "DE",
"countryName": "Germany",
"formatted": "DE811907980",
"registeredName": null,
"memberStateService": "Available",
"reasons": []
}
DE811907980 is the Commission’s own documented example number. Most member states do
not publish the legal name over VIES; registeredName being null is not a failed
lookup, it is the member state declining to share. When VIES is down the same shape comes back with
verdict: "unknown" and a reason that names the transport error — your invoicing code can
branch on the enum instead of guessing from HTTP status.
import Client from 'temsor-api';
const api = new Client();
const r = await api.euVatValidate({ vatNumber: 'DE811907980' });
if (r.verdict === 'unknown') {
// queue and retry — do not add VAT yet
} else if (r.verdict === 'invalid') {
// charge domestic VAT
} else {
// reverse charge
}
What this does not answer
- Which rate to put on the invoice. Registration ≠ rate. Standard, reduced and parking rates live in TEDB (Taxes in Europe Database) and change on a calendar, not when VIES flaps. A valid Hungarian number does not tell you 27% versus 5%.
- OSS / IOSS thresholds. The Union distance-selling threshold (EUR 10,000 from 1 July 2021) is a dated statutory figure, not a VIES field.
- Whether the buyer is the company whose name is on the PO. VIES registration is not KYC. Name matching, when the member state even returns a name, is a hint.
The remaining work in a real billing engine is “given this registered number, which rate and which
OSS rule apply on this invoice date”. That is a table with an asOf, not a live register:
curl -s https://api.temsor.com/v1/eu/vat/rates \
-H 'content-type: application/json' \
-d '{"country":"DE","asOf":"2026-01-15"}'
You get the standard / reduced / parking schedule and the Union OSS threshold (EUR 10,000 from 1 July 2021). You do not get which goods sit in which reduced band — that is TEDB, not this endpoint.
Frequently asked
VIES returned an error. Is the VAT number invalid?
No. An upstream timeout, 5xx or a member-state node marked unavailable means the register did not answer. That is unknown, not invalid. Charging VAT in that window is how you fabricate a reverse-charge dispute.
Does a valid VIES result mean I must reverse-charge?
It means the number is registered, which is the usual precondition for intra-EU reverse charge on a B2B supply. Place of supply, the kind of goods or services, and whether you are in OSS still sit with you — the endpoint does not file the return.
Why is Great Britain (GB) rejected?
GB left VIES after Brexit. Northern Ireland businesses that remain in the VAT area use the XI prefix. A GB number is not a malformed EU number; it is outside the system.
Do I need an API key to try this?
No. Anonymous calls run on a per-IP demo quota. A free key raises it. Results are cached for 24 hours so a retry during a VIES blip does not stampede the Commission’s façade.