VAT API errors fall into two categories: errors you can tell the user about (bad input), and errors that require a fallback strategy (infrastructure failures). The most common mistake is treating all errors the same — either blocking the user for everything or allowing everything through. The right approach is to be strict about user input errors and graceful about infrastructure failures.
The Four Things That Can Go Wrong
| Error | Cause | Correct action |
|---|---|---|
| format_invalid | User entered a malformed VAT number | Show a format hint immediately; do not retry |
| inactive | VAT number is not currently registered | Reject zero-rate claim; show error to user |
| service_unavailable | VIES member state registry is offline | Charge standard VAT; re-validate later |
| Network error / timeout | Your server cannot reach the TaxID API | Same as service_unavailable; never block checkout |
Setting Request Timeouts
Always set an explicit timeout on VAT API calls. VIES queries member states in real time, and some member states respond slower than others. The TaxID API itself has a 10-second internal timeout per VIES request. Set your timeout to 6-7 seconds — long enough to succeed in almost all cases, short enough not to block your users for a noticeably long time.
async function validateVat(vatNumber: string) {
const country = vatNumber.slice(0, 2).toUpperCase();
const vat = vatNumber.replace(/\s/g, '').toUpperCase();
// 6-second timeout — handles slow VIES member states without blocking indefinitely
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 6000);
try {
const res = await fetch(
`https://taxid.dev/api/v1/validate/${country}/${vat}`,
{
headers: { Authorization: `Bearer ${process.env.TAXID_API_KEY}` },
signal: controller.signal,
}
);
if (!res.ok) throw new Error(`API ${res.status}`);
return await res.json();
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
return { valid: false, status: 'service_unavailable', company_name: null };
}
throw err;
} finally {
clearTimeout(timeoutId);
}
}Retry Logic for Network Errors
Retry logic is appropriate for network errors (connection refused, DNS failure, timeout) but NOT for `service_unavailable` status from the API — VIES is down and retrying immediately will not help. Implement at most one retry with a 500ms delay for network-level errors only.
async function validateVatWithRetry(vatNumber: string, maxRetries = 1) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const result = await validateVat(vatNumber);
// API responded — no retry needed even if service_unavailable
return result;
} catch (networkErr) {
// Only retry on actual network errors (fetch threw)
if (attempt < maxRetries) {
await new Promise(r => setTimeout(r, 500));
continue;
}
// All retries exhausted — treat as unavailable
return { valid: false, status: 'service_unavailable', company_name: null };
}
}
}The VIES Fallback Strategy
When `status === 'service_unavailable'`, you have three options ordered from safest to most convenient: (1) charge standard VAT and issue a credit note when VIES recovers; (2) store the VAT number as 'pending' and re-validate asynchronously before the next billing cycle; (3) apply zero-rate provisionally and document the 'good faith' attempt. For e-commerce one-time orders, option 1 is the only safe choice. For subscription billing, option 2 is standard practice.
async function handleCheckoutVat(vatNumber: string, customerId: string) {
const result = await validateVatWithRetry(vatNumber);
if (result.status === 'active') {
return { chargeVat: false, companyName: result.company_name };
}
if (result.status === 'inactive' || result.status === 'format_invalid') {
// User error — show message, do not proceed
throw new Error(`validation_failed:${result.status}`);
}
// service_unavailable: log and queue for re-validation
await db.vatPendingQueue.create({
data: {
customerId,
vatNumber: vatNumber.toUpperCase().replace(/\s/g, ''),
attemptedAt: new Date(),
reason: 'service_unavailable',
},
});
// Charge standard VAT this transaction
return { chargeVat: true, pendingRevalidation: true };
}Monitoring VAT API Health
Track three metrics for VAT API calls: `service_unavailable` rate per hour (spikes indicate VIES outages), `format_invalid` rate (elevated rate may indicate a UX problem with your VAT input field), and p95 response time (should be under 100ms for cached results, under 3s for uncached). Alert on `service_unavailable` rate above 5% over a 15-minute window — that pattern usually indicates a specific EU member state is offline.
Related guides
Start validating EU VAT numbers
Free plan — 100 validations/month. No credit card required.