VAT Number Regex Isn't Validation: What Format Checks Miss
7 Oct 2026

A regex can tell you a string looks like a VAT number. It cannot tell you the number was ever issued, and for some countries it can’t even tell you the digits are internally consistent. Three different things get called ‘VAT validation,’ and conflating them is how integrations end up either rejecting real customers or accepting garbage.
What a regex can actually prove
A VAT number regex checks shape: the right country prefix, the right length, digits where digits belong, letters where letters belong. That’s it. There are three distinct outcomes once you’re dealing with a real VAT number, and a shape regex only resolves the first:
- Malformed – wrong prefix, wrong length, wrong character types. The regex rejects it, correctly, with zero network calls.
- Well-formed but not registered – matches the shape for its country, but no tax authority has that number on file, or it was cancelled. A regex can’t see this; only a live lookup against VIES can.
- Well-formed and registered – matches the shape and VIES confirms it. This is the only outcome that means anything for an actual transaction.
A shape regex can only separate outcome 1 from everything else. It has no way to distinguish 2 from 3, because both are well-formed strings – the difference lives in a government database, not in the string itself. If your validation stops at the regex, you’re one keystroke-perfect fake VAT ID away from treating it as real.
This is a narrower, code-level version of a point already made from the business side in VAT ID Format vs Valid – read that one for the ‘what does a customer-facing form need to tell the user’ framing. This post is about what the regex itself can and can’t do, and what to call each outcome in code.
The EU VAT number pattern, country by country
There’s no single EU VAT number regex. Each of the 27 member states, plus XI for Northern Ireland, has its own prefix and its own body – some fixed-length digit strings, some with a letter mixed in. DE is DE + 9 digits. IE has three live shapes at once (see the Irish VAT number guide for that one specifically). The full per-country table, with an example for every country, is in the EU VAT number formats guide – this post won’t reproduce it.
At the code level, this pattern set isn’t a private lookup table. It’s published as the open-source eu-vat-rates-data npm package, and the same per-country pattern comes back from the vatnode API itself – any GET /v1/vat/{vatId} response carries it under countryVat.vatNumberPattern (illustrative, trimmed):
{
"countryVat": { "vatNumberPattern": "^DE\\d{9}$", ... }
}
Both come from the same dataset:
import { validateFormat } from 'eu-vat-rates-data'
validateFormat('DE123456789') // true — matches DE's pattern
validateFormat('DE12345') // false — too short
One prefix to get right: Greece’s VIES prefix is EL, not the ISO code GR – validateFormat('EL123456789') is true, validateFormat('GR123456789') is false, because GR isn’t a VIES prefix at all.
Where a check digit fits in
Some national VAT number formats embed an arithmetic check digit – a letter or digit computed from the rest of the number, so a single mistyped character usually produces an internally inconsistent string rather than a different valid one. A shape regex doesn’t compute that arithmetic; it only checks that a character sits in the right position and is the right type (digit vs. letter). A string with the wrong check digit but the right shape – right prefix, right length, a letter where a letter goes – passes a shape regex exactly as well as one with the correct check digit.
The local format check here is shape-only – the regexes in eu-vat-rates-data, and the pattern vatnode validates against locally, don’t compute any check digit arithmetic. That’s a real gap: a well-formed string with the wrong check digit passes validateFormat() cleanly. The regex can’t catch it, and neither can vatnode’s local check. Whatever VIES answers comes back as one of the two well-formed responses described below, never the local 400.
That split is exactly why the result is bucketed separately from ‘registered.’ Several of the country-specific posts in this series walk through one national check digit algorithm each, with the real formula – see Ireland’s mod-23 check letter, Italy’s check digit, or Belgium’s mod-97 check. This post won’t repeat those formulas, and you rarely need them: a string with a wrong check digit isn’t a number anyone was issued, so the live check returns it as one of the well-formed outcomes below without you implementing anything. The country posts are there if you want the arithmetic as a free local pre-filter.
Validating in JavaScript/Node
Treat validateFormat() as a cheap first gate, not the validation:
import { validateFormat } from 'eu-vat-rates-data'
function preCheck(rawVatId: string) {
// validateFormat() uppercases internally but doesn't strip separators —
// strip spaces, dots and hyphens before calling it.
const vatId = rawVatId.toUpperCase().replace(/[\s.-]/g, '')
if (!validateFormat(vatId)) {
// Malformed — reject locally, no network call.
return { stage: 'rejected-local' as const, vatId }
}
// Well-formed. Still unknown whether it's registered — that needs a live call.
return { stage: 'passed-local' as const, vatId }
}
preCheck('de 123 456 789') // { stage: 'passed-local', vatId: 'DE123456789' }
preCheck('DE123') // { stage: 'rejected-local', vatId: 'DE123' }
A local pass means ‘worth a network call,’ nothing more. For the full client-and-server flow – debounced input, a backend proxy route, error states – see the Node.js VAT validation guide.
Three responses once you call a real check
Calling the EU VAT API splits a request into three possible responses, and they don’t line up one-to-one with the malformed/not-registered/registered split above – the middle one is new. See the full error code reference for every code vatnode returns, not just these three.
400, no viesCode – rejected before any upstream call. Format is validated locally first; a malformed VAT ID never reaches VIES and never reserves a quota slot. The request ID below is illustrative – in a real response it’s a UUID:
{
"error": {
"code": "INVALID_FORMAT",
"message": "Invalid VAT ID format for DE",
"requestId": "req_abc123"
}
}
No viesCode field on this envelope – that’s the tell that it was caught locally, before any call left vatnode’s servers.
400 with viesCode: "INVALID_INPUT" – VIES itself rejects the input. This is rarer – the string passes the local shape check, but VIES’s own checkVat call still returns INVALID_INPUT for it. This is the response a wrong check digit (or anything else VIES’s own input checks catch that a shape regex can’t) may surface as, though the code shown here doesn’t prove check digits specifically are the cause. Without a requester VAT configured on the account, this also surfaces as INVALID_FORMAT, HTTP 400 – but this time with a viesCode field showing the raw VIES error, and the reserved quota slot is refunded, because no valid/invalid determination was actually delivered:
{
"error": {
"code": "INVALID_FORMAT",
"message": "...",
"viesCode": "INVALID_INPUT",
"requestId": "req_abc123"
}
}
If the account has a requester VAT set in dashboard Account details, the same VIES answer is reported as INVALID_REQUESTER, HTTP 422, instead – when a requester is configured, the API attributes an INVALID_INPUT verdict from VIES to the requester VAT, not the target number being checked.
200 with valid: false – well-formed and VIES answers. This is the normal case, and it’s not an error at all:
{
"valid": false,
"countryCode": "...",
"checkId": "...",
"source": "VIES"
}
valid: false with a 200 status is a real, billable answer – VIES looked the number up and reports it isn’t currently registered. It’s a different thing from the first 400 above, and code that treats every non-2xx-or-valid:true response the same way will log a real ‘not registered’ verdict as if it were a client bug.
FAQ
Is there one EU VAT number regex for every country? No. Each of the 27 member states (plus Northern Ireland’s XI prefix) has its own prefix, length and character layout, so there’s no single pattern that covers all of them – only a map of per-country patterns keyed by prefix.
Does a VAT number passing the regex mean its check digit is correct? Not necessarily. A shape regex only confirms the right prefix, length and character types. Some countries embed an arithmetic check digit in that string, which a shape regex doesn’t compute – it would accept a well-formed string whose check digit is wrong just as readily as one where it’s right.
What does vatnode’s API return for a malformed VAT ID versus a well-formed one that isn’t registered?
A malformed VAT ID – wrong prefix, wrong length, wrong characters – gets a 400 with error code INVALID_FORMAT before any upstream call, so it never touches your quota. A well-formed VAT ID that VIES reports as not currently registered gets a 200 with valid: false – that’s a real, billable answer, not an error.
Should I validate the regex before calling a live API? Yes, as a cheap first gate – it rejects typos and obvious garbage for free, before you spend a network call or a quota slot. It’s not a substitute for the live call; well-formed and registered are two different questions, and only the live call answers the second one.
Next step
A test key is the cheapest way to see the envelope shape for outcome 3 before you spend a live quota slot – fixture VAT numbers under the XX prefix return deterministic valid: true / valid: false responses with no VIES call. Test mode skips the local format check entirely, though: send it anything other than an XX number and you’ll get a 400 explaining that test keys only accept XX fixtures, which is a different error than outcome 1 above and isn’t a verdict on that number’s real format. Register for a free key – free plan, 100 requests/month, no card – or try a single live lookup with no signup at /check.