Guide10 min readAlberto García

VIES API Integration: A RESTful Guide for Developers

VIES uses an archaic SOAP/XML interface that is painful to call directly. This guide explains how a REST wrapper works, what the responses mean, and how to build a production-ready integration.

viesvateuapiguide

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.

StepHandled by VIES directlyHandled by TaxID REST wrapper
ProtocolSOAP/XML over HTTPSJSON REST over HTTPS
Format validationNo — VIES returns errors for bad formatsYes — validated before VIES call
CachingNo native caching23-hour response cache
Outage handlingRaw SOAP faultstatus: service_unavailable
Member state routingManual per-country endpointAutomatic from country prefix
Response normalisationRaw XMLConsistent JSON schema

Making Your First VIES Validation Call

bash
# 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 .
jsonresponse-example.json
{
  "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

PrefixCountryFormat notes
ATAustriaATU + 8 digits (always starts with U)
BEBelgiumBE + 10 digits (may have leading 0)
BGBulgariaBG + 9 or 10 digits
CYCyprusCY + 8 digits + letter
CZCzech RepublicCZ + 8–10 digits
DEGermanyDE + 9 digits
DKDenmarkDK + 8 digits
EEEstoniaEE + 9 digits
ELGreeceEL + 9 digits (not GR — common mistake)
ESSpainES + char + 7 digits + char
FIFinlandFI + 8 digits
FRFranceFR + 2 chars + 9 digits
HRCroatiaHR + 11 digits
HUHungaryHU + 8 digits
IEIrelandIE + complex format with letters
ITItalyIT + 11 digits
LTLithuaniaLT + 9 or 12 digits
LULuxembourgLU + 8 digits
LVLatviaLV + 11 digits
MTMaltaMT + 8 digits
NLNetherlandsNL + 9 digits + B + 2 digits
PLPolandPL + 10 digits
PTPortugalPT + 9 digits
RORomaniaRO + 2–10 digits
SESwedenSE + 12 digits
SISloveniaSI + 8 digits
SKSlovakiaSK + 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:

typescriptlib/vies-handler.ts
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.

typescriptlib/vies-cache.ts
// 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;
}

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.