UK VAT numbers used to be validated through VIES like every other European country. After Brexit they moved to HMRC's own API, which is well-designed for what it does, but comes with some integration overhead that's worth knowing about upfront.
I had customers all over the world, storing thousands of VAT numbers in our SaaS solution, and sometimes a mess, manually managed. That brought me to finding a better way to validate them.
Here's a clean way to handle UK VAT validation, and how to extend it to cover EU and beyond with the same integration.
The HMRC API is solid, but not lightweight
HMRC's VAT validation endpoint lives inside their MTD ("Making Tax Digital") API platform. It's a proper, well-documented API. But it's built for accountancy software integrations, not quick checkout validations. Using it directly means:
- Registering a developer account and creating an application
- Implementing OAuth client credentials flow and handling token refresh
- Going through HMRC's production credentials approval process before going live
The endpoint itself (GET /organisations/vat/check-vat-number/lookup/{targetVrn}) works well, once it's set up... The overhead is just higher than you might expect for a single-country number check.
The other consideration: if you're selling into EU countries as well, you'd need a separate VIES integration alongside it. Two auth systems, two error models, or more.
One endpoint for UK, EU, and more
I use TaxVett for this. It covers UK (HMRC), all 27 EU member states (VIES), Norway, Australia, and Singapore — one REST endpoint, one API key, consistent response format across all of them. For a checkout that sells into multiple regions, that's the main reason to reach for it.
For UK specifically, you just pass the number:
curl -X POST https://api.taxvett.com/v1/validate \
-H "X-Api-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"number": "GB123456789"}'
UK numbers use GB prefix, auto-detected, no country field needed.
Full integration example
Here's a TypeScript module I for a Next.js API route:
// lib/vat.ts
interface ValidationResult {
valid: boolean;
companyName?: string;
companyAddress?: string;
}
export async function validateUKVat(
vatNumber: string
): Promise<ValidationResult> {
// Normalise: strip spaces and ensure GB prefix
const normalised = vatNumber.replace(/\s/g, "").toUpperCase();
const withPrefix = normalised.startsWith("GB")
? normalised
: `GB${normalised}`;
const res = await fetch("https://api.taxvett.com/v1/validate", {
method: "POST",
headers: {
"X-Api-Key": process.env.TAXVETT_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({ number: withPrefix, country: "GB" }),
});
if (res.status === 422) {
// Format validation failed before hitting the registry
return { valid: false };
}
if (!res.ok) {
throw new Error(`TaxVett error: ${res.status}`);
}
const data = await res.json();
return {
valid: data.valid,
companyName: data.company_name,
companyAddress: data.company_address,
};
}
And in the API route:
// pages/api/checkout/validate-vat.ts
import { validateUKVat } from "@/lib/vat";
export default async function handler(req, res) {
const { vatNumber } = req.body;
if (!vatNumber || typeof vatNumber !== "string") {
return res.status(400).json({ error: "vatNumber required" });
}
try {
const result = await validateUKVat(vatNumber);
return res.status(200).json(result);
} catch (err) {
// Don't block checkout if validation service is down
// Log the error, return valid: null to signal uncertainty
console.error("VAT validation error", err);
return res.status(200).json({ valid: null });
}
}
Note: The valid: null pattern is intentional! I'd rather complete a sale and flag it for manual review than reject a legitimate customer because of a network timeout.
What format do UK VAT numbers have?
UK VAT numbers are 9 digits, optionally prefixed with GB. Branch traders use a 12-digit variant (GB + 9 digits + 3-digit branch code). A few formats the normalisation above handles:
| Input | Normalised |
|---|---|
123 4567 89 |
GB123456789 |
gb123456789 |
GB123456789 |
GB 123 4567 89 |
GB123456789 |
HMRC's test number GB999999973 works in sandbox environments if you want to check the integration is wired up before going live.
Does this work for EU VAT too?
That same endpoint actually handles all 27 EU member states, Norway, Australia, and Singapore. If you're selling into multiple regions you validate with one integration and get a consistent response shape regardless of which registry handled the lookup. (I think there are minor differences for just one or a few rare countries, have not affected me in practice so far.)
When it makes sense, and when it doesn't
Good fit:
- You're selling into more than one region
- You're in early development and want to iterate fast
- You want a consistent JSON response
- You don't want to pay to get started
Worth considering:
- If you only need UK and already have an OAuth setup, hitting HMRC directly is a valid option
- VAT numbers pass through a third-party API. If your compliance requirements are strict about data routing, check you are ok with TaxVett's privacy policy before integrating
- At higher volumes the paid plans kick in (Pro €9/month). That's still cheap, but it's not free
Top comments (0)