A European business customer has just entered a VAT number in your Stripe checkout. Your backend now has to decide whether to apply reverse charge, keep VAT on the invoice, flag the customer for review, or reject the input as malformed. A field that looks like a short string has become a tax, billing, and fraud decision.
A useful VAT number example isn't a random sequence copied into a test fixture. It needs a country prefix, a body that follows that country's rules, and a current registration status confirmed by an authoritative service. The European Commission's VIES service is an official search service, not a general database, and only tax administrations can issue VAT numbers. Your production implementation therefore needs both local format validation and a live status lookup.
Table of Contents
- What a VAT Number Example Looks Like
- Anatomy of a VAT Number Across Jurisdictions
- EU Member State VAT Number Formats and Examples
- Non-EU Formats Worth Shipping
- Regex Pitfalls That Break in Production
- Sample API Requests and Responses
- Error Codes and How to Handle Them
- Integrating VAT Validation in Node, Python, and Stripe
- Quick-Reference Table for Shipping Today
- Common Questions Developers Ask
What a VAT Number Example Looks Like
A customer enters de123456789 in a Stripe checkout. Your backend must decide whether the value is malformed, whether to request a live check, and whether the result supports reverse charge. Case handling alone can determine whether a valid customer reaches the tax workflow.
A VAT identification number has three practical layers.
First is the two-letter country prefix. In the EU, it identifies the member state responsible for the number. Greece uses EL in VAT identification numbers, not the country code developers may expect. Normalize the prefix before validation, so lowercase input does not fail only because it was pasted that way.
Next is the country-specific body. Germany uses a nine-digit body, Italy uses eleven digits, Austria requires U after AT, and the Netherlands requires B in the tenth position of its twelve-character pattern. These formats cannot share one global rule. UK guidance on EU VAT numbers and country codes documents the structural differences across jurisdictions.
The final layer is current validity. A string can match a regex yet be unregistered, inactive, mistyped, or unavailable for the transaction context. VIES provides a point-in-time check and cannot confirm whether a number was valid in the past. Treat its response as current status, not a permanent certificate.
Practical rule: Store the normalized number, country, response status, and lookup timestamp. Do not store only a Boolean named
vat_valid.
For unit tests, DE123456789 is a useful canonical example because it exercises the expected shape. It does not prove that the identifier belongs to a real company. Reject obvious malformed input locally, then make the remote request only after the country and body pass the applicable format rule.
That split keeps checkout feedback fast and leaves an audit trail for the tax decision. It also stops a copied example from being treated as evidence that a customer qualifies for reverse charge.
Anatomy of a VAT Number Across Jurisdictions
A production validator should use a staged pipeline: normalize input, validate the country rule locally, then verify registration remotely. This separates formatting errors from status checks and prevents a permissive service response from deciding whether checkout input is acceptable. The European Commission's VAT identification number guidance confirms why a shared global pattern fails: each member state defines its own identifier structure.

Normalize at the boundary. Trim whitespace, uppercase the country code, normalize Unicode, and remove presentation punctuation only when your input policy allows it. Keep the normalized value separate from the raw submission if support staff may need to inspect what the customer entered.
Validate locally before calling a remote service. Check the supplied country, permitted characters, length, required letters, and any country-specific check rule. Local rejection gives immediate feedback and avoids sending obvious typos to a service that may accept a broad input envelope.
Verify remotely only after the local rule passes. VIES checks current EU registration status, while coverage and terminology differ for jurisdictions such as the UK, Switzerland, Norway, and Australia. Store the normalized identifier, country, response, lookup time, and decision context. A Boolean such as vat_valid cannot explain a later invoice or exemption decision.
VIES accepts a broad service-boundary pattern when the country is supplied separately, [0-9A-Za-z\+\*\.]{2,12}. That range is useful for request validation, not for country enforcement. A checkout that relies on it alone can forward structurally invalid input and produce confusing remote errors.
The practical boundary is simple: local rules decide whether the string is shaped correctly, and the authority-backed response decides whether it is registered now. Compare your parser with this EU VAT identification number reference, then record enough request context to reproduce the tax decision. For test fixtures, DE123456789 exercises a German-shaped value, but it does not establish that a real business owns it.
EU Member State VAT Number Formats and Examples
The following catalog is designed for unit tests and middleware checks. The examples are shape examples, not proof of registration. Use the regex after normalizing the value to uppercase and removing presentation spaces or punctuation according to your input policy.
The patterns below reflect the country structures documented in the VAT number format reference. France, Ireland, and Spain need especially careful handling because letters can occupy defined positions rather than appearing only as a final suffix.
| Country | Prefix | Example | Regex |
|---|---|---|---|
| Austria | AT | ATU12345678 |
^ATU\d{8}$ |
| Belgium | BE | BE0123456789 |
^BE\d{10}$ |
| Bulgaria | BG | BG123456789 |
^BG\d{9,10}$ |
| Croatia | HR | HR12345678901 |
^HR\d{11}$ |
| Cyprus | CY | CY12345678A |
^CY\d{8}[A-Z]$ |
| Czechia | CZ | CZ123456789 |
^CZ\d{8,10}$ |
| Denmark | DK | DK12345678 |
^DK\d{8}$ |
| Estonia | EE | EE123456789 |
^EE\d{9}$ |
| Finland | FI | FI12345678 |
^FI\d{8}$ |
| France | FR | FRX1234567890 |
^FR[A-Z0-9]{2}\d{9}$ |
| Germany | DE | DE123456789 |
^DE\d{9}$ |
| Greece | EL | EL123456789 |
^EL\d{9}$ |
| Hungary | HU | HU12345678 |
^HU\d{8}$ |
| Ireland | IE | IE1234567X |
^IE(?:\d[A-Z0-9]\d{5}[A-Z]|\d{7}[A-Z]{1,2})$ |
| Italy | IT | IT12345678901 |
^IT\d{11}$ |
| Latvia | LV | LV12345678901 |
^LV\d{11}$ |
| Lithuania | LT | LT123456789 |
^LT(?:\d{9}|\d{12})$ |
| Luxembourg | LU | LU12345678 |
^LU\d{8}$ |
| Malta | MT | MT12345678 |
^MT\d{8}$ |
| Netherlands | NL | NL123456789B01 |
^NL\d{9}B\d{2}$ |
| Poland | PL | PL1234567890 |
^PL\d{10}$ |
| Portugal | PT | PT123456789 |
^PT\d{9}$ |
| Romania | RO | RO1234567890 |
^RO\d{2,10}$ |
| Slovakia | SK | SK1234567890 |
^SK\d{10}$ |
| Slovenia | SI | SI12345678 |
^SI\d{8}$ |
| Spain | ES | ESX12345678 |
^ES[A-Z0-9]\d{7,8}[A-Z0-9]$ |
| Sweden | SE | SE12345678901 |
^SE\d{10}01$ |
A few details deserve explicit implementation decisions. Austria's identifier is not AT followed by eight arbitrary digits. The U is part of the required structure. The Netherlands pattern isn't a wildcard string either, because the B at position ten must remain fixed. Sweden's local body ends in 01, so a generic twelve-digit pattern can accept values that don't match the documented structure.
France has several permitted arrangements involving letters and digits. The simplified regex above is appropriate for shape screening, but a production validator should defer final status to an authoritative lookup. Spain and Ireland also allow more than one arrangement, so don't reduce them to “digits plus optional letter” without testing every accepted branch.
These examples are best treated as fixtures for the first validation layer. A positive regex result should initiate a VIES request, not an automatic tax exemption.
Non-EU Formats Worth Shipping
A global B2B checkout often collects identifiers outside the EU. The important distinction is operational, not cosmetic. XI belongs to Northern Ireland for relevant EU goods trade and can be checked through VIES, while ordinary UK GB numbers require a separate UK source or commercial provider. The UK is no longer validated through VIES as a general regime.

| Jurisdiction | Canonical shape | Local check |
|---|---|---|
| United Kingdom | GB123456789 |
Format plus HMRC or a separate provider |
| Northern Ireland | XI123456789 |
VIES for the applicable EU goods context |
| Switzerland | CHE-123.456.789 |
Swiss authority or commercial provider |
| Norway | NO123456789 |
Modulus-11 validation plus Norwegian source |
| Australia | ABN12345678901 |
ABN rules and Australian source |
Switzerland's identifier is commonly written with the CHE prefix and nine digits, often displayed with separators. Store a canonical digits-only form plus the display form if invoices need the formatted representation. Don't send Swiss numbers to VIES and interpret a missing VIES result as evidence that the business doesn't exist. It only means the source isn't the right one.
Norway uses a nine-digit organization number with a Modulus-11 check. Format validation can catch transcription errors, but registration status still belongs to a Norwegian source. Australia doesn't use a VAT system in the EU sense, but Australian businesses use an ABN with eleven digits, and EU-facing onboarding flows may still collect it as a business tax identifier.
A country-aware adapter keeps these regimes from contaminating your EU logic:
- Parse the submitted country and identifier.
- Route
XIto the VIES adapter,GBto the UK adapter, and other non-EU values to their relevant source. - Return one internal response model such as
valid,invalid,unknown, orservice_unavailable. - Record which authority produced the result.
Don't label every failed lookup “invalid.” A source outage and an unregistered number lead to different customer and accounting actions.
Regex Pitfalls That Break in Production
A regex can be syntactically correct and still encode the wrong business rule. The most expensive failures happen when a validator rejects legitimate customer input or accepts a string that the remote service can never confirm.
Leading zeros are a classic trap. Germany and Italy preserve their fixed-length numeric bodies, so converting the body to an integer destroys information. Bad:
^DE\d+$
Better:
^DE\d{9}$
For Italy:
^IT\d{11}$
Don't parse a VAT body as a number anywhere in the pipeline. Keep it as a string from request parsing through database storage and API submission.
Austria's U is mandatory. This accepts too much:
^AT\d{8,9}$
Use:
^ATU\d{8}$
The letter isn't decoration. Dropping it creates a value that may look plausible in a UI but doesn't match the documented Austrian structure.
The Netherlands needs a fixed B. This weak pattern allows the wrong character:
^NL\d{9}[A-Z]\d{2}$
The stricter version is:
^NL\d{9}B\d{2}$
France also defeats simplistic “digits only” assumptions because valid structures can contain letters in early positions. Sweden requires the final 01, so ^SE\d{12}$ is too broad. Use ^SE\d{10}01$ after the country prefix is included.
Normalization matters just as much as the expression. Convert fullwidth digits to ordinary ASCII where your application permits it, reject unsupported Unicode characters, trim whitespace, uppercase the country code, and decide whether punctuation is accepted before removing it. Developers auditing input handling can also review this guide to avoid regex pitfalls in validation, even though its subject is broader than VAT identifiers.
The right test suite includes valid shapes, invalid lengths, misplaced letters, lowercase input, formatted input, leading-zero bodies, and Unicode copy-paste cases. Then run a separate integration suite against mocked remote responses. Regex should protect the API, not impersonate it.
Sample API Requests and Responses
VIES exposes a SOAP interface. A direct request makes the boundary visible:
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:urn="urn:ec.europa.eu:taxud:vies:services:checkVat:types">
<soapenv:Body>
<urn:checkVat>
<urn:countryCode>DE</urn:countryCode>
<urn:vatNumber>123456789</urn:vatNumber>
</urn:checkVat>
</soapenv:Body>
</soapenv:Envelope>
For approximate matching, the operation is typically checkVatApprox, with the relevant country, VAT number, and optional company or address fields in the SOAP body. Wrappers hide those details, but your error handling still needs to account for SOAP faults and unavailable service responses.
A REST wrapper gives application code a simpler contract. For example, TaxID documents a request shaped like this:
POST https://api.taxid.dev/v1/validate
Content-Type: application/json
{
"country": "DE",
"vatNumber": "DE123456789"
}
A successful response can be normalized into a record like:
{
"valid": true,
"companyName": "Example GmbH",
"address": {
"street": "Example Street",
"city": "Berlin",
"country": "DE"
}
}
A curl request is useful for checking credentials and payload serialization:
curl -X POST https://api.taxid.dev/v1/validate \
-H "Authorization: Bearer $TAXID_API_KEY" \
-H "Content-Type: application/json" \
-d '{"country":"DE","vatNumber":"DE123456789"}'
Node callers should distinguish an HTTP transport failure from a valid response whose valid field is false:
const response = await fetch("https://api.taxid.dev/v1/validate", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.TAXID_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
country: "DE",
vatNumber: "DE123456789"
})
});
if (!response.ok) {
throw new Error(`Validation request failed: ${response.status}`);
}
const result = await response.json();
An invalid result should remain machine-readable, for example:
{
"valid": false,
"error": {
"code": "vat_invalid",
"message": "VAT number could not be validated"
}
}
If VIES is unavailable, don't convert the outage into valid: false. Return an availability error and let the checkout policy decide whether to hold the order, collect VAT, or queue a retry.
Error Codes and How to Handle Them
VIES responses can contain brittle text and service-level failures. Your application should translate them into stable internal codes before Stripe, invoicing, or customer-support workflows consume them.
| Internal code | Meaning | Checkout behavior |
|---|---|---|
vat_invalid |
Format or status failed | Show a correction message, don't retry automatically |
vat_unknown |
No usable record returned | Ask for confirmation or route to review |
service_unavailable |
Timeout, SOAP fault, or provider outage | Retry with exponential backoff |
rate_limited |
Too many requests | Use cached results and delay the next request |
country_unsupported |
No adapter exists | Keep the tax decision manual or use a supported source |
vat_invalid should be deterministic from the customer's perspective. A malformed value won't become valid through retries, so return a field-level error and preserve the submitted value for correction. vat_unknown is different. It may represent an authority response that doesn't expose a record, so a supplier workflow can ask for documentation instead of rejecting the vendor immediately.
For service_unavailable, use bounded exponential backoff with jitter and a maximum attempt policy. Don't make a customer wait on a long chain of synchronous SOAP retries. A checkout can create a pending verification state, while a background worker retries and updates the customer record.
Caching is the main defense against duplicated requests. Cache a normalized identifier and result according to your compliance policy, invalidate it when the customer changes the number or country, and avoid treating a cached positive response as proof that the number was valid on a different historical date.
TaxID-style envelopes make these outcomes easier to consume because your code can switch on error.code rather than parse provider-specific text. Keep the original provider response in structured logs, but expose only the stable internal contract to frontend and billing code.
Integrating VAT Validation in Node, Python, and Stripe
The safest Stripe flow validates the customer's identifier before creating a Checkout Session. The application should set tax-related fields only after a positive response, not after a regex match.

A Node route can gate session creation:
const validation = await fetch("https://api.taxid.dev/v1/validate", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.TAXID_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ country: "DE", vatNumber: customerVat })
}).then(r => r.json());
const session = await stripe.checkout.sessions.create({
mode: "subscription",
customer: stripeCustomerId,
line_items: lineItems,
customer_update: { name: "auto", address: "auto" },
tax_id_collection: { enabled: true },
automatic_tax: { enabled: true },
...(validation.valid ? { customer_creation: "always" } : {})
});
The exact Stripe tax configuration depends on your registration and invoicing model. The invariant is simpler, don't mark a customer tax-exempt because the input merely resembles a valid identifier.
Python with requests can use the same contract and an idempotency key for the surrounding billing operation:
import requests
payload = {
"country": "DE",
"vatNumber": customer_vat,
}
result = requests.post(
"https://api.taxid.dev/v1/validate",
headers={
"Authorization": f"Bearer {TAXID_API_KEY}",
"Content-Type": "application/json",
},
json=payload,
timeout=5,
).json()
if result.get("valid"):
create_checkout_session(idempotency_key=order_key, tax_exempt=True)
else:
create_checkout_session(idempotency_key=order_key, tax_exempt=False)
A cached validation path can sit inside checkout, while raw VIES calls are better isolated in asynchronous jobs or supplier screening. Teams already operating Stripe can compare this pattern with a broader Stripe integration with Halo AI, but the VAT decision must remain explicit in your own backend.
TaxID provides a single REST integration surface and country-specific checks before remote validation, documented in its VAT API integrations. The operational contract should be: validate when the customer saves the tax ID, re-validate when issuing an invoice, and never trust format alone.
Quick-Reference Table for Shipping Today
Use this table as a compact wiki artifact. The examples are structural fixtures, and the source column tells you where a status lookup belongs.
| Country | Prefix | Example | Regex | Source |
|---|---|---|---|---|
| Austria | AT | ATU12345678 |
^ATU\d{8}$ |
VIES |
| Belgium | BE | BE0123456789 |
^BE\d{10}$ |
VIES |
| Bulgaria | BG | BG123456789 |
^BG\d{9,10}$ |
VIES |
| Croatia | HR | HR12345678901 |
^HR\d{11}$ |
VIES |
| Cyprus | CY | CY12345678A |
^CY\d{8}[A-Z]$ |
VIES |
| Czechia | CZ | CZ123456789 |
^CZ\d{8,10}$ |
VIES |
| Denmark | DK | DK12345678 |
^DK\d{8}$ |
VIES |
| Estonia | EE | EE123456789 |
^EE\d{9}$ |
VIES |
| Finland | FI | FI12345678 |
^FI\d{8}$ |
VIES |
| France | FR | FRX1234567890 |
^FR[A-Z0-9]{2}\d{9}$ |
VIES |
| Germany | DE | DE123456789 |
^DE\d{9}$ |
VIES |
| Greece | EL | EL123456789 |
^EL\d{9}$ |
VIES |
| Hungary | HU | HU12345678 |
^HU\d{8}$ |
VIES |
| Ireland | IE | IE1234567X |
^IE\d{7}[A-Z]{1,2}$ |
VIES |
| Italy | IT | IT12345678901 |
^IT\d{11}$ |
VIES |
| Latvia | LV | LV12345678901 |
^LV\d{11}$ |
VIES |
| Lithuania | LT | LT123456789 |
^LT(?:\d{9}|\d{12})$ |
VIES |
| Luxembourg | LU | LU12345678 |
^LU\d{8}$ |
VIES |
| Malta | MT | MT12345678 |
^MT\d{8}$ |
VIES |
| Netherlands | NL | NL123456789B01 |
^NL\d{9}B\d{2}$ |
VIES |
| Poland | PL | PL1234567890 |
^PL\d{10}$ |
VIES |
| Portugal | PT | PT123456789 |
^PT\d{9}$ |
VIES |
| Romania | RO | RO1234567890 |
^RO\d{2,10}$ |
VIES |
| Slovakia | SK | SK1234567890 |
^SK\d{10}$ |
VIES |
| Slovenia | SI | SI12345678 |
^SI\d{8}$ |
VIES |
| Spain | ES | ESX12345678 |
^ES[A-Z0-9]\d{7,8}[A-Z0-9]$ |
VIES |
| Sweden | SE | SE12345678901 |
^SE\d{10}01$ |
VIES |
| United Kingdom | GB | GB123456789 |
^GB\d{9}$ |
HMRC or provider |
| Northern Ireland | XI | XI123456789 |
^XI\d{9}$ |
VIES for applicable EU goods |
| Switzerland | CHE | CHE123456789 |
^CHE\d{9}$ |
Swiss source |
| Norway | NO | NO123456789 |
^NO\d{9}$ |
Norwegian source |
| Australia | ABN | ABN12345678901 |
^ABN\d{11}$ |
Australian source |
Common Questions Developers Ask
Is VIES validation permanent
No. VIES is a point-in-time status check, and the Commission states that it can't confirm whether a number was valid in the past. Save the lookup timestamp and recheck when the tax decision matters again.
Can I use XI for services
Treat XI as the Northern Ireland prefix for the relevant EU goods-trade context. Don't assume that an XI result automatically settles the place-of-supply or VAT treatment of every service. Those rules require a separate tax decision.
Is format-only validation acceptable for B2B invoicing
No, not when the invoice or reverse-charge decision depends on the customer's registration status. Regex can reject malformed input, but only an appropriate authority-backed lookup can provide the current status signal.
How often should a VAT number be revalidated
Revalidate when the customer changes the identifier, before a material invoice or exemption decision, and whenever your compliance policy requires a fresh status check. Cache responsibly to avoid duplicate calls, but don't treat an old positive result as current forever.
TaxID provides a developer-facing REST endpoint for VAT and company identification validation, returning structured validity results and, where available, company details. Test your checkout and invoice workflow with TaxID, then replace format-only tax decisions with a logged, current validation step.