You're in the middle of checkout or supplier onboarding, and a customer has just pasted a tax number into the field. Finance wants the invoice exempted, support wants the error gone, and engineering wants to know whether this is VAT, corporation tax, or just malformed input. If you ship this wrong, you either block a valid business customer or apply tax relief you can't defend later.
Table of Contents
- What a Company Tax Number Actually Is
- Country Formats You Must Validate First
- Doing a Manual VIES Check the Right Way
- Programmatic Validation With TaxID in Node and Python
- Reading Machine-Readable Error Codes Like Stripe
- Caching, Retries, and Surviving VIES Outages
- Checkout and B2B Invoicing Patterns With Troubleshooting
What a Company Tax Number Actually Is
A lot of teams search for how to check company tax number and really mean one of two different identifiers. In practice, the number they need for billing and VAT exemption is usually a VAT number, while a corporation tax reference is a separate administrative identifier used for company tax filing. If you validate the wrong one, your workflow can look “successful” while still failing the actual compliance requirement.

VAT numbers drive billing decisions
The practical reason VAT matters is simple. The European Commission operates VIES as the cross-border information exchange service for VAT registration data, and HMRC's checker shows the kind of output teams need, the registered company name, address, and registration status, plus the UK format rules for a VAT number. The UK government also requires VAT-registered businesses to display their VAT registration number on VAT invoices, so this is not just a lookup, it's a gate before tax treatment changes. HMRC's VAT check guidance is the right place to anchor your internal logic.
That's why the phrase check company tax number usually maps to VAT validation in SaaS checkout flows, marketplace onboarding, and B2B invoicing. Reverse-charge decisions depend on VAT registration, not on corporation tax status. A valid VAT result can support exempt billing, while a corporation tax reference tells you something else entirely.
Corporation tax references solve a different problem
A UK corporation tax number is a 10-digit HMRC reference, and it's usually found in HMRC letters, online accounts, or by contacting HMRC directly. It is not what VIES validates. If your product is deciding whether to apply VAT rules at checkout, corporation tax data won't help.
That distinction is where many integration tickets go sideways. Product managers hear “tax number,” support hears “company number,” and engineering gets handed a field that tries to serve both. Keep the identifier types separate in your data model, name the field clearly, and only run VAT logic when the workflow needs VAT status.
Practical rule: If the user needs reverse charge, invoice VAT display, or B2B tax exemption, validate VAT. If the user needs company tax administration, you're in a different workflow entirely.
For a broader primer on identifiers, the cleanest companion read is What Is a Tax Identification Number.
Country Formats You Must Validate First
A powerful validator starts before any network call. Country-specific formats tell you whether the input is even worth sending to VIES or HMRC, and they save you from parsing avoidable failures. If you skip normalization, you end up paying for bad requests and debugging responses that were doomed from the start.
Normalize before lookup
User input rarely arrives in a clean registry-ready form. People paste spaces, dots, prefixes, and local formatting quirks, then expect a working result. Strip separators, remove the country prefix where the target system expects it, and store the normalized version separately from the display version.
| Country | Format | Prefix expected by VIES |
|---|---|---|
| Austria | VAT number with country code plus local digits | AT |
| Belgium | VAT number with country code plus local digits | BE |
| Germany | VAT number with country code plus local digits | DE |
| Greece | VAT number with local digits, often with a leading zero in user input | EL |
| Spain | VAT number can mix letters and digits | ES |
| United Kingdom | 9 digits or 12 characters with the GB prefix in the broader UK format, but HMRC's checker expects the 9-digit number without the GB prefix | GB |
| Switzerland | Country-specific tax ID structure outside VIES VAT member-state checks | CH |
| Norway | Country-specific tax ID structure outside VIES VAT member-state checks | NO |
| Australia | Country-specific tax ID structure outside VIES VAT member-state checks | AU |
The key behavior is simple. VIES expects the country code concatenated with the number, while HMRC's checker wants the 9-digit VAT number without the GB prefix for lookup. That split is why frontend validators should normalize input first and only then decide which backend path to call. The EU guidance on VIES format expectations is the best reference for that flow. EU VIES validation guidance
Don't trust naive regex alone
Regex can catch obvious junk, but it won't understand every country's edge cases. Greek numbers can trip you up if you don't handle the leading zero users type. Spanish identifiers can include letters as well as digits, so an all-numeric assumption breaks valid customers. If your regex is too strict, support gets tickets from companies that are real, registered, and blocked by your own input layer.
Treat format validation as a low-cost filter, not as proof of validity.
The production pattern is straightforward. Validate the shape locally, normalize separators and prefixes, then send only plausible candidates to the registry or API. That keeps your error logs readable and makes compliance failures much easier to explain to finance.
Doing a Manual VIES Check the Right Way
If you only need to check one number right now, the official VIES portal is enough. It's useful for a human verification step, but it's a poor building block for a production checkout because the underlying service is SOAP, the response is XML, and the service doesn't give you the kind of modern operational guarantees most billing flows expect. The VAT VIES check guide is a good companion if you need the portal workflow in plain language.
What VIES actually returns
The practical output matters more than the portal UI. A successful lookup can return whether the VAT number is valid, plus the registered name and address when the member state provides them. You also get request metadata such as a request identifier and a request date. Those fields are useful for audit logs, invoice records, and support tickets, because they let you prove what was checked and when.
That still leaves a major gap. VIES tells you the registration status, but it doesn't turn your checkout into a resilient system. If the service is unavailable, your user still wants to pay, and your billing code still needs to decide whether to proceed, retry, or stop. Manual lookup helps for one-off verification, not for a real customer path.
Where the portal falls short in production
The biggest trade-off is operational uncertainty. There's no comfortable uptime promise to build around, and outages can last long enough that a team needs a fallback plan rather than optimism. XML parsing also creates avoidable failure modes in modern Node or Python stacks, especially when your real application already speaks JSON everywhere else.
Use manual VIES when a human is reconciling a supplier record or confirming a suspicious submission. Don't use it as your only integration layer if the number is part of checkout, invoice generation, or automated onboarding. That's where a wrapper becomes the right abstraction, because it can turn brittle service behavior into stable product behavior.
Programmatic Validation With TaxID in Node and Python
Production validation should be boring. Your app sends the country code and the raw number, gets JSON back, and uses the result to decide whether a buyer qualifies for VAT exemption or whether an invoice should keep tax applied. One option for that workflow is TaxID's VAT ID checker, which wraps registry lookups into a REST-style response with normalized fields.
Node.js example
import express from "express";
const app = express();
app.use(express.json());
app.post("/api/validate-vat", async (req, res) => {
const { countryCode, vatNumber } = req.body;
const response = await fetch("https://api.taxid.dev/vat/check", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.TAXID_API_KEY}`
},
body: JSON.stringify({ countryCode, vatNumber })
});
const result = await response.json();
if (result.valid) {
return res.json({
status: "ok",
registeredName: result.name,
address: result.address
});
}
return res.status(400).json({
status: "invalid",
error: result.error
});
});
In a Stripe checkout flow, you'd store the validation outcome in metadata or your invoice draft, then only apply reverse charge when the buyer is in another EU country and the VAT result is valid. Keep that decision in one place, not scattered across UI and billing jobs. That way support can replay the exact logic later.
Python example
from flask import Flask, request, jsonify
import requests
import os
app = Flask(__name__)
@app.post("/api/validate-vat")
def validate_vat():
payload = request.get_json()
response = requests.post(
"https://api.taxid.dev/vat/check",
headers={
"Authorization": f"Bearer {os.environ['TAXID_API_KEY']}",
"Content-Type": "application/json"
},
json={
"countryCode": payload["countryCode"],
"vatNumber": payload["vatNumber"]
},
timeout=10
)
result = response.json()
if result.get("valid"):
return jsonify({
"status": "ok",
"registeredName": result.get("name"),
"address": result.get("address")
})
return jsonify({
"status": "invalid",
"error": result.get("error")
}), 400
The important part isn't the framework. It's the contract. Your app needs a deterministic JSON response that includes the validation status, the registered name, and the address when available. Once you have that, the rest of the billing code can stay clean and testable.
Implementation rule: validate early, store the raw input, store the normalized form, and store the registry result separately.
Reading Machine-Readable Error Codes Like Stripe
Text errors are fragile. A line like “VAT identification number is not valid” is fine for a human, but it's a bad machine contract because wording changes, translations change, and parsing rules break. Structured codes are better because your app can branch on behavior instead of English phrasing.

The codes that matter
A Stripe-style error model makes the response easier to wire into checkout and invoicing logic. vat_invalid means the number fails validation. vat_country_mismatch means the country code doesn't match the member-state structure you sent. vat_blocked means the registry rejects the number as blocked. service_unavailable means your lookup could not be completed. rate_limited means you need to slow down and retry later.
Each code needs a different product response. Invalid input should reject the exemption and prompt the user to fix the number. A country mismatch should point the user at the country selector or the normalized prefix logic. A blocked number often belongs in manual review, because a billing system shouldn't approve it without review.
Map errors to actions, not messages
| Error code | What it means | Product reaction |
|---|---|---|
| vat_invalid | The number fails checksum or format validation | Reject exemption and ask for a corrected number |
| vat_country_mismatch | The country code and number structure don't belong together | Prompt the user to correct the country or prefix |
| vat_blocked | The registry blocks the number | Route to manual review |
| service_unavailable | The registry or wrapper can't complete the lookup | Retry, then fall back to cached evidence or review |
| rate_limited | Too many requests in a short period | Back off and retry later |
The mistake to avoid is treating service_unavailable like vat_invalid. Those are not the same problem, and collapsing them into one bucket creates compliance risk. A lookup failure says nothing about the business's tax status, it only says your verification path didn't finish.
Build the decision tree into your application, not your support playbook. Then your checkout, invoicing, and supplier systems can all react consistently when the registry or wrapper says something went wrong.
Caching, Retries, and Surviving VIES Outages
A validator that calls a registry on every request is expensive, slow, and fragile. The production pattern is to cache previous successful lookups, retry transient failures carefully, and make a conscious decision about what to do when the external service is down. If you're validating the same supplier or customer repeatedly, cache is not an optimization, it's core infrastructure.

Cache the result, not the guess
The most practical setup is a Redis-backed cache with a 24-hour TTL for repeated lookups. That gives your app a fast path for the same VAT number during checkout retries, invoice regeneration, and supplier updates. TaxID's product notes describe cached lookups returning in sub-10 ms when the result is already stored, which is exactly the kind of behavior a checkout flow needs when the user is waiting. TaxID's service overview
Cache the normalized request key, not just the visible input. That means the same VAT number pasted with spaces or dots still hits the same record after normalization. Keep the original registry response as proof, because support and finance may need to know what was validated and when.
Retries should be narrow and controlled
Retries make sense for transient failures such as service_unavailable or temporary registry issues. They do not make sense for an obviously invalid number, because more attempts won't fix bad input. Use short exponential backoff, retry only the transient class, and stop before you turn a short outage into a traffic storm.
A useful circuit-breaker pattern is simple. If VIES is down, treat a customer as exempt only if you already have a previous valid lookup on record. If there's no prior proof, fall back to manual review or charge tax until a validation result is available. That keeps you from approving a brand-new number on blind faith during an outage.
Don't silently downgrade a registry outage into a validity failure. Those are different operational states, and your billing policy should reflect that difference.
This is also where unit economics show up. Every uncached call consumes your own time and your provider's capacity, and every outage that forces a healthy buyer into a taxable checkout can hurt trust. The right resilience layer makes validation feel invisible to the customer and predictable to the finance team.
Checkout and B2B Invoicing Patterns With Troubleshooting
Checkout and invoicing need different timing, but they should share the same validation core. In checkout, validate the VAT field on blur, show feedback quickly, and apply reverse charge only when the lookup says the number is valid. In invoicing, re-check supplier IDs before payout or invoice generation so you don't freeze a bad assumption into an accounting record.

Two flows, one validation engine
A checkout flow should be strict about the user-facing field and forgiving about transient backend failures. If the number is invalid, block the exemption and tell the buyer to fix the input. If the registry is down and you already have a cached valid result, use it and log the fallback. A billing system that behaves consistently here is much easier for finance to audit later.
An invoice pipeline is more conservative. Validate again when the invoice is created, log every attempt, and attach the proof to the invoice record. If the number can't be confirmed, route it to review instead of guessing. That's especially important when a supplier changes entity details or a marketplace receives invoices from many different jurisdictions.
Troubleshooting the cases teams actually hit
- Valid but not searchable yet: The number may be valid but not immediately visible in the registry. In that case, keep the transaction in review rather than marking it invalid.
- No tax number yet: Some businesses haven't been issued one. Capture the company name and route them to a manual path instead of forcing a false lookup.
- Non-EU seller onboarding: Don't assume VAT validation will answer every compliance question for non-EU entities. You may need a separate onboarding rule set.
- Inactive versus not found: Those are different states operationally. Keep them separate in your logs so support can tell whether the registry rejected the lookup or the business really isn't there.
- Fallback by company name: Official registries can sometimes help with a business name, but that doesn't solve every onboarding edge case. Use it as a fallback, not as your primary proof.
The strongest pattern is to make validation visible in logs, but invisible in the happy path. That gives finance the evidence it needs and keeps buyers from getting stuck on a tax field that shouldn't feel like a dead end.
If you want to stop hand-rolling VAT validation, caching, and error handling around VIES, TaxID exposes a single API that returns validation status, company name, and address in clean JSON. Visit TaxID to wire the check into your checkout or invoicing flow and keep your billing logic resilient when registry lookups fail.