Guide9 min readAlberto García

VAT API Error Handling: Timeouts, Fallbacks & Retry Strategies

The four things that can go wrong with a VAT API call and exactly what to do about each: format errors, invalid registrations, registry outages, and network failures.

error-handlingvatapiproductionguide

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

ErrorCauseCorrect action
format_invalidUser entered a malformed VAT numberShow a format hint immediately; do not retry
inactiveVAT number is not currently registeredReject zero-rate claim; show error to user
service_unavailableVIES member state registry is offlineCharge standard VAT; re-validate later
Network error / timeoutYour server cannot reach the TaxID APISame 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.

typescriptlib/vat-with-timeout.ts
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.

typescriptlib/vat-with-retry.ts
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.

typescriptlib/vat-fallback.ts
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.

Start validating EU VAT numbers

Free plan — 100 validations/month. No credit card required.

AG
Alberto García

Founder, TaxID

Building EU VAT validation tools for developers. Obsessed with compliance automation and developer experience.