Guide28 min readTaxID Team

VAT Number Format a Developer's Reference Guide

A complete developer's guide to the VAT number format for every EU country, UK, CH, NO, & AU. Includes regex, examples, and a robust validation workflow.

vat number formatvies validationtax id apieu vatdeveloper guide

You're probably here because a VAT field just landed in your backlog.

Maybe it's a B2B SaaS signup flow, a Stripe-powered checkout, or invoice generation for EU customers. It looks simple at first. Add an input, run a regex, maybe call VIES, done. Then reality shows up. Customers paste spaces and punctuation, prefixes don't match the country they selected, one market uses letters in the middle, another uses a branch format, and your direct VIES integration returns an error that's useless in production.

That's why VAT number format isn't just reference data. It's an engineering problem involving normalization, country rules, remote validation, error handling, and operational resilience. If your implementation is brittle, you get failed checkouts, bad invoice data, support tickets, and tax logic you can't trust later.

Table of Contents

Why VAT Number Format Matters in Your Code

A customer enters a VAT ID at checkout, your form accepts it, and the order goes through as tax-exempt. Later, finance finds the number was malformed for that country, the invoice is wrong, and support has to clean up the account by hand. That failure usually starts in code, with input handling that is too loose in one place and too strict in another.

VAT number format determines tax treatment, invoice data, and whether a B2B transaction can be processed without charging VAT up front. In practice, this is not a formatting detail. It affects checkout logic, account state, and the audit trail you keep for cross-border sales.

The implementation mistake is usually simple. Teams treat VAT IDs as one field with one validation rule. That works until real customer data hits the system. Country-specific structures differ, prefixes are not always what developers expect, and a regex that looked fine in tests starts rejecting valid registrations or accepting bad ones.

There is also a hard split between format validation and registry validation. Format validation answers, "Could this be a valid VAT number for this jurisdiction?" Registry validation answers, "Is this business currently registered?" Those checks belong in different stages of the pipeline, with different failure handling and different user messages.

Practical rule: Reject obvious junk locally. Call a registry only after normalization and country-specific format checks pass.

That split matters because raw VIES lookups are slow, occasionally unavailable, and not helpful when the input is obviously broken. If every keystroke or every form submit depends on an external service, the user experiences your tax stack as an unreliable form. I have seen this create duplicate support tickets, manual invoice corrections, and brittle retry logic that should never have existed.

A better approach is straightforward. Accept messy real-world input, normalize it safely, validate against the right national pattern, then verify status with an API built for production use, such as TaxID, instead of wiring your checkout directly to raw registry behavior. That keeps bad data out of your billing system and reduces the long tail of fixes that follows one bad VAT field.

Anatomy of a VAT Number

A VAT number usually has two parts. First comes a country prefix. Then comes a country-defined block of digits or letters. That sounds simple until you have to validate real input from multiple jurisdictions.

The shared shape is only a starting point. Each tax authority defines its own structure, length, and character rules, and those rules do not line up cleanly across Europe or with nearby markets. Some numbers are digits only. Some include letters in fixed positions. Some use check characters or suffixes that break a generic parser.

Prefixes are not always as obvious as they look

Prefixes are usually close to ISO country codes, but billing systems hit exceptions fast. Greece appears as EL in VAT contexts. Northern Ireland uses XI for VAT numbers used in EU trade. Code that assumes every valid prefix matches the country selector one-to-one will reject legitimate registrations.

A few examples show why hardcoded assumptions fail:

Jurisdiction Example structure
Germany DE + 9 digits
France FR + 11 characters
Netherlands NL + 9 digits + B + 2 digits
UK commonly 9 digits, with additional special formats

The French entry is a good reminder. If you model VAT numbers as "two letters plus digits," France will cause trouble. The first part after FR can include letters, and that detail matters in production.

A VAT number is a parsing problem before it is a validation problem

In practice, I treat a VAT number as structured input with jurisdiction-specific rules, not as a text field that happens to start with two letters. Users paste spaces, punctuation, local formatting, and sometimes prefixes that do not match the selected country. Good code separates those concerns cleanly.

A broad regex such as ^[A-Z]{2}[A-Z0-9]+$ still has value. It catches obvious junk early and keeps bad requests away from downstream systems. But it does not tell you whether NL123456789B01 is plausible for the Netherlands, whether FR is followed by a valid character pattern, or whether a number is using a trade-specific prefix like XI correctly.

The maintainable approach is to store the anatomy explicitly:

  • prefix
  • normalized national part
  • jurisdiction rule
  • optional checksum or suffix logic

That design holds up better than one giant regex table glued into form validation. It also makes registry lookups and later audit work much easier, because your code knows what it extracted and why a candidate passed or failed.

A billing system should be boring here. Parse deterministically, validate against the exact country rule, and leave the final registration check to the verification step.

The Developer's VAT Validation Workflow

A customer pastes DE 123/456/789 into checkout, your frontend accepts it, and the tax engine rejects the invoice five minutes later. That is the kind of failure that creates support tickets, manual corrections, and bad audit trails. VAT validation works better as a staged server-side workflow with clear states at each step.

An infographic illustrating the seven-step developer workflow for validating VAT numbers through API services.

Start permissive in the UI and strict in the backend

The browser should reduce obvious input mistakes. The backend should make the decision.

In production, the workflow usually looks like this:

  1. Collect the raw input
    Accept pasted values as users enter them. Spaces, punctuation, and mixed case are normal.

  2. Normalize on the server
    Remove separators, uppercase letters, and produce one canonical value for validation and storage. Keep the original input separately if your billing or compliance flow needs an audit trail.

  3. Run a cheap structure check
    Reject input that has no plausible country prefix or contains impossible characters. This is the point of broad guards, not final validation.

  4. Apply the exact country rule
    Match the normalized value against the selected jurisdiction or the detected prefix, depending on your UX. Country-specific logic belongs in this step, not in ad hoc frontend regexes spread across forms.

  5. Verify against a remote source
    Query a registry-backed service only after the candidate passes local checks. That saves calls, reduces noise in logs, and makes error handling much simpler.

For implementation examples across EU identifiers, this European VAT numbers guide for developers is a useful companion reference.

Keep format validation and registration checks separate

These are different outcomes and they should stay different in code. A VAT number can match the expected pattern and still fail verification because it is inactive, not registered, or temporarily unavailable from the upstream service.

That distinction becomes important as soon as you integrate with VIES directly. Raw VIES calls can time out, return inconsistent availability, or give you too little information to explain the failure cleanly to users. If your model collapses everything into one boolean, you lose the ability to retry safely or present the right message.

Use explicit states instead:

  • Format invalid means the input does not match the jurisdiction rule after normalization.
  • Format valid, registry invalid means the shape is acceptable but the registration check failed.
  • Registry unavailable means your system could not confirm status and needs a retry or fallback path.
  • Validated means local checks passed and the remote verification succeeded.

DIY validation often falters beyond basic checks. A regex table is manageable. A full workflow with normalization, country exceptions, retries, timeout handling, and stored verification metadata is harder to keep correct over time. Teams that outgrow raw VIES integrations usually move to an API layer such as TaxID because it gives them one place to handle formatting rules, verification calls, and operational edge cases.

A minimal server-side record often includes:

  • normalized_value
  • input_country
  • detected_prefix
  • format_status
  • registry_status
  • validated_name
  • validated_address
  • last_checked_at

That schema avoids the common vat_valid = true shortcut, which is too vague to support billing logic, customer support, or audit review.

EU VAT Number Format Quick Reference

If you need a fast implementation reference, use a country table and keep it versioned in code. Don't scatter VAT rules across form components, webhook handlers, and invoice templates.

For a broader walkthrough of European identifiers and implementation context, this European VAT numbers guide is useful as a companion reference.

Copy paste reference table

The table below is designed for format checking only. It's not proof of registration. Several patterns in the wild have edge cases, so treat these regexes as practical validation starters for application code, then pair them with remote verification.

Country Code Format Structure Regex Pattern Example
Austria AT AT + U + 8 digits ^ATU\d{8}$ ATU12345678
Belgium BE BE + 10 digits ^BE\d{10}$ BE0123456789
Bulgaria BG BG + 9 or 10 digits ^BG\d{9,10}$ BG123456789
Croatia HR HR + 11 digits ^HR\d{11}$ HR12345678901
Cyprus CY CY + 8 digits + 1 letter ^CY\d{8}[A-Z]$ CY12345678X
Czechia CZ CZ + 8 to 10 digits ^CZ\d{8,10}$ CZ12345678
Denmark DK DK + 8 digits ^DK\d{8}$ DK12345678
Estonia EE EE + 9 digits ^EE\d{9}$ EE123456789
Finland FI FI + 8 digits ^FI\d{8}$ FI12345678
France FR FR + 11 characters ^FR[A-HJ-NP-Z0-9]{2}\d{9}$ FRAB123456789
Germany DE DE + 9 digits ^DE\d{9}$ DE123456789
Greece EL EL + 9 digits ^EL\d{9}$ EL123456789
Hungary HU HU + 8 digits ^HU\d{8}$ HU12345678
Ireland IE IE + country-specific alphanumeric pattern ^IE[0-9A-Z+*]{8,9}$ IE1234567AB
Italy IT IT + 11 digits ^IT\d{11}$ IT12345678901
Latvia LV LV + 11 digits ^LV\d{11}$ LV12345678901
Lithuania LT LT + 9 or 12 digits ^LT(\d{9}|\d{12})$ LT123456789
Luxembourg LU LU + 8 digits ^LU\d{8}$ LU12345678
Malta MT MT + 8 digits ^MT\d{8}$ MT12345678
Netherlands NL NL + 9 digits + B + 2 digits ^NL\d{9}B\d{2}$ NL123456789B01
Poland PL PL + 10 digits ^PL\d{10}$ PL1234567890
Portugal PT PT + 9 digits ^PT\d{9}$ PT123456789
Romania RO RO + 2 to 10 digits ^RO\d{2,10}$ RO12345678
Slovakia SK SK + 10 digits ^SK\d{10}$ SK1234567890
Slovenia SI SI + 8 digits ^SI\d{8}$ SI12345678
Spain ES ES + 1 letter or digit + 7 digits + 1 letter or digit ^ES[A-Z0-9]\d{7}[A-Z0-9]$ ESA1234567Z
Sweden SE SE + 12 digits ^SE\d{12}$ SE123456789012

A few implementation notes matter more than the regex itself:

  • Store canonical form. Keep the prefix as part of the saved value.
  • Don't infer too much. If a customer selects France but enters DE..., block and explain.
  • Version your rules. Validation logic belongs in one module with tests.

The regex table gets you to “probably well-formed.” It doesn't get you to “registered and usable for reverse charge.”

Formats for UK Switzerland Norway and Australia

A checkout form accepts XI123456789, your regex only allows GB, and the customer gets blocked even though the number is valid for the transaction you are trying to process. That kind of bug shows up late, usually after finance asks why a legitimate B2B account was taxed incorrectly or why onboarding failed for a UK entity.

Non-EU tax IDs are where many in-house validators start to fray. Teams often build a clean EU regex table, then bolt on the UK, Switzerland, Norway, and Australia as exceptions. That works for a while, but these markets have enough edge cases that exception handling turns into core logic. Store the wrong canonical form, or reject a valid prefix, and the problem leaks into invoicing, reverse-charge logic, and support queues.

The UK causes the most trouble. Production code needs to handle standard 9-digit numbers, 12-digit branch formats, the older GD and HA variants, and XI for Northern Ireland in EU-related trade contexts. A validator limited to ^GB\d{9}$ is incomplete. If you need the UK-specific details and examples, keep this UK VAT number format reference for developers close to the code.

Reference table for common non-EU markets

Country Code Format Structure Regex Pattern Example
United Kingdom GB GB + 9 digits ^GB\d{9}$ GB123456789
United Kingdom branch GB GB + 12 digits ^GB\d{12}$ GB123456789000
United Kingdom government GD GD + 3 digits ^GD\d{3}$ GD001
United Kingdom health authority HA HA + 3 digits ^HA\d{3}$ HA599
Northern Ireland XI XI + 9 digits ^XI\d{9}$ XI123456789
Switzerland CHE CHE + 9 digits, optional tax suffix in some presentations ^CHE\d{9}$ CHE123456789
Norway NO NO + 9 digits ^NO\d{9}$ NO123456789
Australia ABN 11 digits ^\d{11}$ 12345678901

Switzerland needs a normalization rule, not just a regex. In real input, Swiss IDs often arrive as CHE123456789 MWST, CHE123456789 TVA, or CHE123456789 IVA. The suffix is useful for display and for matching what a customer copied from a document. It is usually noise for storage and validation. Strip the trailing tax label, keep CHE plus the digits, and validate the canonical value.

Australia is also easy to mishandle because the ABN does not look like a VAT number at all. It still belongs in the same billing pipeline if your product validates business tax identifiers across markets. I usually keep the parser country-aware and the storage model generic. One field for canonical tax ID, one field for country, one field for validation result, and one verification status from the upstream service.

That design choice matters once you stop relying on regex alone. Raw pattern checks can tell you whether NO123456789 looks plausible. They cannot tell you whether the identifier is active, whether the prefix is appropriate for the transaction, or whether your upstream verification service is timing out. Such circumstances typically make DIY validation brittle, and an API-backed workflow starts paying for itself.

Common Pitfalls and Normalization Rules

Most VAT bugs don't come from the regex. They come from everything around it. Input handling, country mismatch logic, stale assumptions about prefixes, and upstream verification failures cause more trouble than the pattern itself.

An infographic showing six essential rules for standardizing and normalizing VAT numbers for accurate data processing.

Normalization rules that should happen before validation

You want one canonicalization function used everywhere. Checkout, admin forms, CSV imports, supplier onboarding, and API ingestion should all go through the same code path.

A good baseline is:

  • Strip separators. Remove spaces, dashes, slashes, and dots if your product accepts human-friendly formatting.
  • Uppercase everything. Prefixes and suffix markers should be case-insensitive at input time.
  • Validate or infer the prefix consistently. If the user selected a billing country, confirm the entered prefix matches.
  • Keep leading zeros intact. They matter in several jurisdictions.
  • Reject unsupported symbols early. Don't let punctuation drift downstream.
  • Store one canonical string. That should be the value used for dedupe, lookups, and invoice generation.

Here's a practical Node-style normalization shape:

{
  "raw": " nl 123456789 b01 ",
  "normalized": "NL123456789B01",
  "country": "NL"
}

That part is straightforward. Teams usually get into trouble when they start mixing display formatting with validation formatting.

What regex alone will never solve

A regex can tell you whether NL123456789B01 looks like a Dutch VAT number. It cannot tell you whether it's active, whether the name returned by the registry matches your customer's company name closely enough, or whether the upstream validation service is available.

It also won't solve national subtleties. The Netherlands is a good example. Published guidance distinguishes the VAT identification number from the VAT tax number used internally for administration, so developers need to be careful about which identifier a customer is entering. If your support team says “the Dutch customer gave us a BTW number and validation failed,” the first question should be which Dutch identifier they provided.

When validation fails for a real customer, assume your assumptions are wrong before you assume the customer is.

Another pitfall is treating VIES availability as guaranteed. It isn't. If your system directly depends on a remote registry during checkout, you need a fallback policy. Common options include allowing checkout but marking tax status as pending, or blocking exemption until validation can be retried. Which policy is right depends on your risk tolerance and invoicing process.

DIY wrappers often age badly because every exception gets patched in locally. One rule for XI. Another for Swiss suffixes. Another for branch trader formats. A retry handler for remote outages. After a while, the validator becomes a pile of conditionals nobody wants to touch.

Solving Validation Challenges with the TaxID API

If you've integrated raw VIES before, you already know the rough edges. SOAP isn't pleasant to work with in modern app stacks, error handling is inconsistent, and production code ends up full of parsing and retry logic that has nothing to do with your product.

Screenshot from https://www.taxid.dev

A cleaner pattern is to put a stable JSON-facing layer between your application and registry-backed validation. One option is TaxID VAT API, which validates VAT and company identification numbers across the EU member states plus the UK, Switzerland, Norway, and Australia through a single REST endpoint. The practical value for developers is not marketing copy. It's having one response model, one error model, and one integration surface instead of hand-rolling country logic around VIES.

What a cleaner integration looks like

The useful design choice is separating concerns:

  • your app handles form UX, country selection, and persistence
  • your normalization layer produces a canonical identifier
  • the API handles format-aware validation and registry interaction
  • your business logic interprets the result into tax decisions

That means your checkout service doesn't need to understand SOAP, parse brittle failure text, or implement special-case retries around VIES outages. It just needs to handle explicit states such as valid, invalid, or temporarily unavailable.

A typical backend call flow looks like this:

  1. Receive raw input from checkout.
  2. Normalize to canonical form.
  3. Send canonical value to the validation API.
  4. Persist validation status, legal name, and address if returned.
  5. Apply tax logic based on the result.

Later in the flow, a walkthrough helps more than prose.

Example response shape

What developers usually want back is boring JSON. Something like this:

{
  "country_code": "DE",
  "vat_number": "DE123456789",
  "format_valid": true,
  "registered": true,
  "company_name": "Example GmbH",
  "company_address": "Berlin, Germany",
  "error": null
}

And for failures:

{
  "country_code": "DE",
  "vat_number": "DE123",
  "format_valid": false,
  "registered": false,
  "company_name": null,
  "company_address": null,
  "error": {
    "code": "vat_invalid",
    "message": "VAT number format is invalid for country"
  }
}

The exact field names vary by provider, but the shape should be this clean. If your current integration leaks transport-level weirdness into your app, the abstraction is wrong.

Developer FAQ for VAT Number Handling

What should the app do when validation is unavailable

Don't collapse “service unavailable” into “customer entered an invalid VAT number.” Those are different events. Store a distinct status, let the user continue only if your finance process can tolerate pending verification, and queue a retry.

What's the practical difference between invalid format and inactive number

Invalid format means your own format layer rejected the input before any authoritative lookup. Inactive or unregistered means the structure passed, but the registry-backed validation did not confirm the number. That distinction should appear in both your logs and your user-facing messages.

Should I store the normalized value or the raw input

Store both if you can. Use the normalized value for validation, search, dedupe, and invoice generation. Keep the raw input only if it helps with support or audit trails.

How often do VAT number formats change

Not often enough to justify panic, but often enough that hardcoded validation tables shouldn't be forgotten. Keep rules centralized, tested, and easy to update.

Should checkout block on a failed remote lookup

It depends on your risk policy. For many teams, blocking on obvious format errors makes sense, while temporary remote failures should move the customer into a manual review or deferred verification path instead of forcing a hard stop.

Do I need one validator for every market

No. You need one validation workflow with market-specific rules inside it. The architecture should be unified even when the actual identifier formats are not.


If you're building checkout, invoicing, or supplier validation for multiple markets, TaxID gives you a simpler way to handle VAT and company ID verification without maintaining your own country-rule matrix and registry wrapper.

AG
Alberto García

Founder, TaxID

Building EU VAT validation tools for developers. Obsessed with compliance automation and developer experience.