VIES — the EU's VAT Information Exchange System — is the authoritative source for EU VAT number validation. It is operated by the European Commission and queries each EU member state's national tax authority in real time. The catch: VIES exposes a SOAP/XML interface that was designed in the early 2000s and has not been modernised. Parsing SOAP envelopes, handling XML namespaces, managing member-state-level outages, and building retry logic takes days of work. A REST wrapper like the TaxID API abstracts all of that into a single JSON endpoint.
How the REST Wrapper Works
When you call `GET https://taxid.dev/api/v1/validate/DE/DE123456789`, the API does the following: it validates the format against the country's VAT number specification, checks an internal cache for a recent result, and if there is no cached result, it calls the VIES SOAP endpoint for the `DE` member state. The SOAP response is parsed, normalised into a consistent JSON structure, cached for up to 23 hours (matching the VIES cache window), and returned as JSON.
| Step | Handled by VIES directly | Handled by TaxID REST wrapper |
|---|---|---|
| Protocol | SOAP/XML over HTTPS | JSON REST over HTTPS |
| Format validation | No — VIES returns errors for bad formats | Yes — validated before VIES call |
| Caching | No native caching | 23-hour response cache |
| Outage handling | Raw SOAP fault | status: service_unavailable |
| Member state routing | Manual per-country endpoint | Automatic from country prefix |
| Response normalisation | Raw XML | Consistent JSON schema |
Making Your First VIES Validation Call
# Replace DE123456789 with the actual VAT number to validate
curl -s -H "Authorization: Bearer $TAXID_API_KEY" \
"https://taxid.dev/api/v1/validate/DE/DE123456789" | jq .{
"valid": true,
"status": "active",
"vat": "DE123456789",
"country_code": "DE",
"company_name": "Example GmbH",
"address": "Musterstraße 1\n10115 Berlin",
"cached": false,
"request_id": "req_7f3a9b2c1d"
}All 27 EU Member State Prefixes
| Prefix | Country | Format notes |
|---|---|---|
| AT | Austria | ATU + 8 digits (always starts with U) |
| BE | Belgium | BE + 10 digits (may have leading 0) |
| BG | Bulgaria | BG + 9 or 10 digits |
| CY | Cyprus | CY + 8 digits + letter |
| CZ | Czech Republic | CZ + 8–10 digits |
| DE | Germany | DE + 9 digits |
| DK | Denmark | DK + 8 digits |
| EE | Estonia | EE + 9 digits |
| EL | Greece | EL + 9 digits (not GR — common mistake) |
| ES | Spain | ES + char + 7 digits + char |
| FI | Finland | FI + 8 digits |
| FR | France | FR + 2 chars + 9 digits |
| HR | Croatia | HR + 11 digits |
| HU | Hungary | HU + 8 digits |
| IE | Ireland | IE + complex format with letters |
| IT | Italy | IT + 11 digits |
| LT | Lithuania | LT + 9 or 12 digits |
| LU | Luxembourg | LU + 8 digits |
| LV | Latvia | LV + 11 digits |
| MT | Malta | MT + 8 digits |
| NL | Netherlands | NL + 9 digits + B + 2 digits |
| PL | Poland | PL + 10 digits |
| PT | Portugal | PT + 9 digits |
| RO | Romania | RO + 2–10 digits |
| SE | Sweden | SE + 12 digits |
| SI | Slovenia | SI + 8 digits |
| SK | Slovakia | SK + 10 digits |
Warning
Greece uses the prefix `EL`, not `GR`. Sending `GR` will return `format_invalid`. The TaxID API accepts both `EL` and `GR` and normalises automatically, but if you are building your own routing logic, use `EL`.
Handling VIES Status Codes
VIES can return different fault codes for different conditions. The TaxID REST wrapper normalises all of these into four clean status values:
import type { VatResponse } from './types';
export function handleViesResult(result: VatResponse) {
switch (result.status) {
case 'active':
// Number is valid and currently registered in VIES
// Safe to apply zero-rate / reverse charge
return { canApplyZeroRate: true, company: result.company_name };
case 'inactive':
// Number was valid but registration has lapsed or been cancelled
// Cannot zero-rate — charge standard local VAT
return { canApplyZeroRate: false, reason: 'deregistered' };
case 'format_invalid':
// Number does not match the expected format for this country
// Usually a user input error — show format guidance
return { canApplyZeroRate: false, reason: 'bad_format' };
case 'service_unavailable':
// VIES member state node is temporarily offline
// DO NOT assume invalid — charge VAT and re-validate when VIES recovers
return { canApplyZeroRate: false, reason: 'unavailable', retry: true };
}
}Caching Strategy for Production
VIES itself caches results for up to 23 hours — validated numbers do not change status within a day. The TaxID API mirrors this cache window. For production applications, add a second layer of caching in your own infrastructure to avoid re-calling the API for the same VAT number within a 24-hour window.
// Redis caching layer in front of the TaxID API
import { Redis } from '@upstash/redis';
const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL!, token: process.env.UPSTASH_REDIS_REST_TOKEN! });
export async function validateWithCache(vatNumber: string) {
const key = `vat:${vatNumber.toUpperCase().replace(/\s/g, '')}`;
const cached = await redis.get(key);
if (cached) return { ...cached, fromCache: true };
const result = await fetch(
`https://taxid.dev/api/v1/validate/${vatNumber.slice(0,2)}/${vatNumber}`,
{ headers: { Authorization: `Bearer ${process.env.TAXID_API_KEY}` } }
).then(r => r.json());
// Only cache definitive results — not service_unavailable
if (result.status === 'active' || result.status === 'inactive') {
await redis.setex(key, 23 * 3600, result);
}
return result;
}Related guides
Start validating EU VAT numbers
Free plan — 100 validations/month. No credit card required.