API-integraties · 14 min lezen
BTW-nummer valideren met de VIES API — retry-logica en caching
Door Alex · Gepubliceerd 17 april 2026
Een BTW-nummer kun je syntactisch valideren met een regex, maar dat zegt niets over of het nummer actief is en daadwerkelijk bij een geregistreerde ondernemer hoort. Voor die laatste stap bestaat één Europese bron: het VIES-systeem van de Europese Commissie. Dit artikel behandelt de structuur van de Nederlandse BTW-identificatie, het gebruik van de REST-endpoint van VIES, typische faalmodes (429, 503, MS_UNAVAILABLE) en een pragmatische caching-strategie zodat je niet bij elke request de EU belast.
1. Wat is VIES?
VIES staat voor VAT Information Exchange System. Het is een gratis dienst van de Europese Commissie waarmee je kunt controleren of een BTW-identificatienummer uit een van de 27 lidstaten op dit moment geldig en actief is. VIES zelf beheert géén database: het doorzoekt de nationale registers (in Nederland de Belastingdienst) via een gestandaardiseerd protocol en geeft een ja/nee-antwoord terug, eventueel met naam en adres van de houder als de lidstaat dat publiceert.
Historisch werd VIES aangesproken via SOAP (WSDL endpoint op ec.europa.eu/taxation_customs/vies/checkVatService.wsdl). Sinds 2024 biedt de Commissie ook een REST-endpoint aan, dat voor nieuwe integraties de voorkeur verdient omdat het minder ceremonie kost dan SOAP-envelopes opbouwen.
2. Structuur van een Nederlands BTW-nummer
Een Nederlands BTW-identificatienummer bestaat uit 14 tekens:
NL + 9 cijfers + B + 2 cijfers
─── ───────── ─── ─────
│ │ │ │
landcode RSIN suffix volgnummer vestigingDe negen cijfers vormen het Rechtspersonen en Samenwerkingsverbanden Informatienummer (RSIN) voor rechtspersonen. Voor eenmanszaken is het basisdeel wiskundig gelijk aan het BSN van de ondernemer — een erfenis van vóór 2020 die bekend stond als het "oude BTW-nummer". Sinds 1 januari 2020 krijgen eenmanszaken een nieuw BTW-identificatienummer dat losstaat van het BSN, om privacyredenen. Het oude nummer bleef intern bij de Belastingdienst in gebruik als omzetbelastingnummer.
De letter B staat vast. De laatste twee cijfers zijn het volgnummer van de vestiging of fiscale eenheid — meestal 01, maar bij een fiscale eenheid of meerdere vestigingen kan dat oplopen. Voor VIES-validatie stuur je het volledige nummer zonder "NL" als vatNumber en apart "NL" als countryCode.
3. VIES REST-endpoint
Het REST-endpoint volgt het patroon:
GET https://ec.europa.eu/taxation_customs/vies/rest-api/ms/{countryCode}/vat/{vatNumber}
Accept: application/jsonEen geldig antwoord heeft ongeveer deze vorm:
{
"isValid": true,
"requestDate": "2026-04-17T10:22:13.517Z",
"userError": "VALID",
"name": "VOORBEELD B.V.",
"address": "HOOFDSTRAAT 1\n1234 AB AMSTERDAM",
"requestIdentifier": "WAPIAAAW7_2026_04_17_11223"
}De velden name en address kunnen leeg zijn — lidstaten mogen zelf beslissen of zij die gegevens vrijgeven. Nederland doet dat wel, Duitsland niet. Het veld requestIdentifier is het bewijsstuk dat je moet bewaren als je wilt aantonen dat je BTW-identificatienummer op het moment van de transactie gecontroleerd was.
4. TypeScript implementatie
Een type-safe basisimplementatie, zonder retry of cache — die komen in de volgende twee secties:
export interface ViesResult {
isValid: boolean;
requestDate: string;
name?: string;
address?: string;
requestIdentifier?: string;
userError?: string;
}
const VIES_BASE = 'https://ec.europa.eu/taxation_customs/vies/rest-api/ms';
export async function checkVatNumber(
countryCode: string,
vatNumber: string,
): Promise<ViesResult> {
const cc = countryCode.toUpperCase();
const num = vatNumber.replace(/\s+/g, '').toUpperCase();
const url = `${VIES_BASE}/${cc}/vat/${num}`;
const res = await fetch(url, {
headers: { Accept: 'application/json' },
});
if (!res.ok) {
throw new ViesHttpError(res.status, await res.text());
}
return (await res.json()) as ViesResult;
}
export class ViesHttpError extends Error {
constructor(public status: number, public body: string) {
super(`VIES returned ${status}`);
}
}Drie dingen om op te merken: het endpoint verwacht hoofdletters in de landcode, de witruimte moet eruit (gebruikers plakken regelmatig een BTW-nummer met spaties of streepjes), en het response-schema is niet bindend — de EU kan extra velden toevoegen. Daarom gebruikt de interface ? voor alle niet-gegarandeerde velden.
5. Fouten en retry-logica
VIES is een verzameling van 27 nationale databases, opgehangen aan een centrale router. Ieder subsysteem kan tijdelijk uitvallen, en de EU-router gooit dan een specifieke status terug. De voornaamste foutcategorieën:
| Status | Betekenis | Retry? |
|---|---|---|
| 200 + isValid:false | Nummer niet (meer) geregistreerd | Nee |
| 400 INVALID_INPUT | Syntactisch onjuist BTW-nummer | Nee |
| 429 | Rate limit per IP overschreden | Ja, met backoff |
| 503 MS_UNAVAILABLE | Nationale database tijdelijk offline | Ja, met backoff |
| 503 SERVICE_UNAVAILABLE | VIES-router zelf offline | Ja, met backoff |
| 504 | Timeout richting lidstaat | Ja, één keer |
Exponential backoff met jitter is een veilige default. Een compacte implementatie:
const RETRY_STATUSES = new Set([429, 503, 504]);
export async function checkVatNumberWithRetry(
countryCode: string,
vatNumber: string,
maxAttempts = 4,
): Promise<ViesResult> {
let lastError: unknown;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await checkVatNumber(countryCode, vatNumber);
} catch (err) {
lastError = err;
const retryable =
err instanceof ViesHttpError && RETRY_STATUSES.has(err.status);
if (!retryable || attempt === maxAttempts - 1) throw err;
// Exponential backoff: 500ms, 1s, 2s, 4s — plus jitter
const base = 500 * 2 ** attempt;
const jitter = Math.random() * 250;
await new Promise((r) => setTimeout(r, base + jitter));
}
}
throw lastError;
}Let op: de EU publiceert geen harde rate limit, maar onofficieel circuleert een vuistregel van ongeveer één request per seconde per IP voor duurzaam gebruik. Bij pieken tolereert de dienst meer, maar gedraag je als een nette API-client.
6. Caching-strategie
Drie redenen om te cachen: latency (500–2000 ms per call is normaal), rate limiting (een backoffice die bij elke factuur VIES aanroept loopt snel tegen 429's aan) en robuustheid (als een lidstaat een dag uitvalt, wil je niet dat je facturatie daarmee stilvalt).
De Commissie hanteert intern een richttermijn van 24 uur voor updates in het VIES- antwoord, maar in de praktijk zijn BTW-registraties dagen tot weken in omloop voordat ze intrekken. Een TTL van één week dekt de meeste usecases, en een TTL van 30 dagen is acceptabel voor niet-kritische workflows (bv. marketing-leads kwalificeren).
Aanbevolen TTL per usecase:
- Intracommunautaire factuur (0% BTW toepassen): 24 uur — juridisch belang groot
- CRM / leads: 7 dagen — acceptabel
- Historische auditlog: nooit cachen, altijd live — je moet het
requestIdentifierper transactie bewaren
Een simpel Redis- of Cloudflare-KV-patroon:
const TTL_SECONDS = 7 * 24 * 60 * 60; // 7 dagen
export async function checkVatCached(
countryCode: string,
vatNumber: string,
kv: { get(k: string): Promise<string | null>; put(k: string, v: string, opts: { expirationTtl: number }): Promise<void> },
): Promise<ViesResult> {
const key = `vies:${countryCode}:${vatNumber}`;
const cached = await kv.get(key);
if (cached) return JSON.parse(cached) as ViesResult;
const fresh = await checkVatNumberWithRetry(countryCode, vatNumber);
// Negatieve antwoorden korter cachen — de registratie kan net ingaan
const ttl = fresh.isValid ? TTL_SECONDS : 60 * 60;
await kv.put(key, JSON.stringify(fresh), { expirationTtl: ttl });
return fresh;
}Twee bewust gemaakte keuzes: negatieve antwoorden krijgen een kortere TTL (1 uur), zodat een net-geregistreerd BTW-nummer niet een week in de cache blijft staan. En de sleutel bevat landcode en nummer afzonderlijk, zodat je per lidstaat kunt invalideren als je weet dat die partij onderhoud heeft gehad.
7. Juridische context — wanneer moet je valideren?
Voor intracommunautaire leveringen geldt dat de leverancier 0 % BTW mag factureren als de afnemer in een andere lidstaat geregistreerd is als ondernemer. Sinds de zogenaamde "quick fixes" die met ingang van 2020 in de BTW-richtlijn (Richtlijn 2006/112/EG, artikel 138) zijn opgenomen, is een geldig VIES-nummer een materiële voorwaarde geworden voor die nultarief-toepassing. Voorheen was het een formaliteit die met andere bewijsstukken kon worden aangevuld — nu niet meer.
Concreet betekent dit: als je factureert zonder BTW en achteraf blijkt het VIES-nummer ongeldig of ingetrokken, kan de Belastingdienst de 21 % BTW alsnog naheffen — bij de leverancier. De bewijslast ligt bij de ondernemer. Het requestIdentifier dat VIES teruggeeft, samen met de timestamp van de controle, is in die discussie je enige harde bewijsstuk. Bewaar het minimaal zeven jaar samen met de factuur.
Artikel 138 schrijft daarnaast voor dat het vervoer van de goederen aantoonbaar moet zijn, en artikel 139–141 regelen uitzonderingen zoals de driehoekstransactie. Voor de toepassing van 0 % BTW is een VIES-check dus noodzakelijk maar niet voldoende.
8. Bronnen
- Europese Commissie — VIES on-the-web, te raadplegen via ec.europa.eu/taxation_customs/vies.
- Richtlijn 2006/112/EG van de Raad van 28 november 2006 betreffende het gemeenschappelijke stelsel van belasting over de toegevoegde waarde, artikelen 138–141 (intracommunautaire leveringen).
- Uitvoeringsverordening (EU) 282/2011, met name artikel 18 over het vaststellen van de hoedanigheid van belastingplichtige afnemer.
- Belastingdienst — toelichting op het nieuwe BTW-identificatienummer voor eenmanszaken (invoering 2020).
Fout gevonden of aanvullende bron? Mail via contact.
Gerelateerde tools
- BTW-nummer Generator — genereer fictieve NL BTW-nummers voor tests
- KVK-nummer Generator — genereer fictieve KVK-nummers (vaak samen gebruikt met BTW-nummers)
- De elfproef ontleed — waarom het RSIN-deel van BTW-nummers wiskundig op BSN lijkt
Nieuwe artikelen in je inbox
Max. 1 mail per maand. Geen spam. Uitschrijven in 1 klik.