Dev.to WebDev 🛠 Dev 👁 0 📖 5 min read

EU VAT number formats for every country (with regex), and what a VIES check really tells you

If you sell to businesses in the EU, sooner or later you have to check a customer's VAT number: at checkout, before you zero-rate an invoice, or when you clean up a customer list. This post is the reference we wanted whi

If you sell to businesses in the EU, sooner or later you have to check a customer's VAT number: at checkout, before you zero-rate an invoice, or when you clean up a customer list. This post is the reference we wanted while building a VAT check: the format of every country's number, a regex for each, and what a live check against the EU's VIES service actually tells you.

Disclosure up front: we build a small VAT and company check API at SpellzCode, and it shows up near the end. Everything before that works without it.

Two different checks

A VAT number can be wrong in two ways.

  1. It is malformed. A digit is missing, or the prefix is wrong. You can catch this offline with a pattern.
  2. It is well formed but not registered. Only the member state's tax database knows. The EU exposes those databases through VIES (VAT Information Exchange System).

Do the first check in your form, so people get instant feedback. Do the second on your server before you rely on the number.

The format of every VIES country

Strip spaces, dots and hyphens and uppercase the input first. People type DE 123 456 789 and se556703-7485 01.

Country Prefix After the prefix Regex
Austria AT the letter U and 8 digits ^ATU\d{8}$
Belgium BE 10 digits, the first 0 or 1 ^BE[01]\d{9}$
Bulgaria BG 9 or 10 digits ^BG\d{9,10}$
Cyprus CY 8 digits and a letter ^CY\d{8}[A-Z]$
Czech Republic CZ 8, 9 or 10 digits ^CZ\d{8,10}$
Germany DE 9 digits ^DE\d{9}$
Denmark DK 8 digits ^DK\d{8}$
Estonia EE 9 digits ^EE\d{9}$
Greece EL 9 digits ^EL\d{9}$
Spain ES 9 characters: a letter or digit, 7 digits, a letter or digit ^ES[A-Z0-9]\d{7}[A-Z0-9]$
Finland FI 8 digits ^FI\d{8}$
France FR a 2-character key (digits, or letters other than O and I) and the 9-digit SIREN ^FR[0-9A-HJ-NP-Z]{2}\d{9}$
Croatia HR 11 digits ^HR\d{11}$
Hungary HU 8 digits ^HU\d{8}$
Ireland IE 7 digits and 1 or 2 letters, or the older form: a digit, a letter or + or *, 5 digits and a letter `^IE(\d{7}[A-Z]{1,2}\
Italy IT 11 digits {% raw %}^IT\d{11}$
Lithuania LT 9 or 12 digits `^LT(\d{9}\
Luxembourg LU 8 digits {% raw %}^LU\d{8}$
Latvia LV 11 digits ^LV\d{11}$
Malta MT 8 digits ^MT\d{8}$
Netherlands NL 9 digits, the letter B and 2 digits ^NL\d{9}B\d{2}$
Poland PL 10 digits ^PL\d{10}$
Portugal PT 9 digits ^PT\d{9}$
Romania RO 2 to 10 digits ^RO\d{2,10}$
Sweden SE 12 digits: the 10-digit organisationsnummer followed by 01 ^SE\d{12}$
Slovenia SI 8 digits ^SI\d{8}$
Slovakia SK 10 digits ^SK\d{10}$
Northern Ireland XI 9 or 12 digits, or GD or HA and 3 digits `^XI(\d{9}\

As a JavaScript object:
{% raw %}

const VAT_PATTERNS = {
  AT: /^ATU\d{8}$/,
  BE: /^BE[01]\d{9}$/,
  BG: /^BG\d{9,10}$/,
  CY: /^CY\d{8}[A-Z]$/,
  CZ: /^CZ\d{8,10}$/,
  DE: /^DE\d{9}$/,
  DK: /^DK\d{8}$/,
  EE: /^EE\d{9}$/,
  EL: /^EL\d{9}$/,
  ES: /^ES[A-Z0-9]\d{7}[A-Z0-9]$/,
  FI: /^FI\d{8}$/,
  FR: /^FR[0-9A-HJ-NP-Z]{2}\d{9}$/,
  HR: /^HR\d{11}$/,
  HU: /^HU\d{8}$/,
  IE: /^IE(\d{7}[A-Z]{1,2}|\d[A-Z+*]\d{5}[A-Z])$/,
  IT: /^IT\d{11}$/,
  LT: /^LT(\d{9}|\d{12})$/,
  LU: /^LU\d{8}$/,
  LV: /^LV\d{11}$/,
  MT: /^MT\d{8}$/,
  NL: /^NL\d{9}B\d{2}$/,
  PL: /^PL\d{10}$/,
  PT: /^PT\d{9}$/,
  RO: /^RO\d{2,10}$/,
  SE: /^SE\d{12}$/,
  SI: /^SI\d{8}$/,
  SK: /^SK\d{10}$/,
  XI: /^XI(\d{9}|\d{12}|(GD|HA)\d{3})$/,
};

function looksLikeVat(input) {
  const vat = input.toUpperCase().replace(/[\s.\-]/g, "").replace(/^GR/, "EL");
  const pattern = VAT_PATTERNS[vat.slice(0, 2)];
  return Boolean(pattern && pattern.test(vat));
}

Things that trip people up

  • Greece is EL, not GR. VIES uses EL. Accept GR from users and convert it.
  • Northern Ireland is XI. Since Brexit, GB numbers are not in VIES. Only Northern Ireland businesses trading in goods have an XI number.
  • Germany and Spain return no name or address. A VIES check for a DE or ES number tells you valid or not valid and nothing else. Do not build a flow that needs the company name for those two.
  • The German Steuernummer is not the VAT number. Invoices often carry both. Only the USt-IdNr. (DE plus 9 digits) can be checked.
  • Sweden, Finland and France embed the company number. A Swedish VAT number is the 10-digit organisationsnummer plus 01. A Finnish one is the business ID without its hyphen. A French one is a 2-character key plus the 9-digit SIREN, and the key is (12 + 3 * (SIREN % 97)) % 97.
  • A pattern match is not a registration. DE123456789 matches the German pattern and is not a real company.

The live check

VIES has a SOAP service and, more recently, a REST endpoint. Both work, and both have the same operational quirk: each member state runs its own backend, and they go down or refuse concurrent requests independently. You will see answers like MS_UNAVAILABLE and MS_MAX_CONCURRENT_REQ.

The rule that matters: an error is not "not valid". If Germany's backend is busy, you know nothing about the number. Retry with a short backoff, and if you still have no answer, tell the user you could not check it. Never reject a customer because VIES timed out.

Cache positive answers for a while. A number that was valid ten minutes ago is almost certainly still valid, and VIES is not fast.

If you would rather not run that yourself

This is the part where our own tool comes in. We wrapped the above (normalising input, retrying busy member states, caching clear answers, never turning an outage into "not valid") into one GET request:

curl -H "Authorization: Bearer $API_KEY" \
  "https://tools.funwords.eu/vat/validate?vat=SE556703748501"
{
  "vat": "SE556703748501",
  "country": "SE",
  "number": "556703748501",
  "valid": true,
  "name": "Spotify AB",
  "address": "REGERINGSGATAN 19, 111 53 STOCKHOLM",
  "source": "EU VIES (European Commission)"
}

A failed check answers with a 5xx and is not charged, so your code can treat it as "no answer". There is a free check form and a page per country (format, local name, the quirks above) here: EU VAT number check. The API itself, with Python, Node.js and PHP examples, is here: Company check API. It also looks companies up in the Swedish, Norwegian, Finnish and French registers.

If you only need the format check, the table above is all you need. Corrections are welcome in the comments: tax authorities change formats now and then, and we would rather fix the table than leave a wrong regex on the internet.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.