Dutch VAT Number Validation: A Developer's Guide to VIES and BTW
How to validate Dutch BTW numbers using the VIES API, common formats, and integration patterns for KYC and invoicing workflows.
Dutch VAT Number Validation: A Developer’s Guide to VIES and BTW
If your application handles invoices, payments, or customer onboarding for Dutch businesses, validating VAT numbers is not optional. EU tax law requires you to verify the VAT status of business customers before applying the reverse charge mechanism on cross-border transactions. Get it wrong and you may owe the VAT yourself.
This guide covers the Dutch VAT (BTW) number format, how VIES validation works, and practical code for integrating it into your workflow.
Understanding the Dutch BTW Number Format
In the Netherlands, the VAT number is called a BTW-nummer (Belasting Toegevoegde Waarde). It follows a specific format:
NL + 9 digits + B + 2 digits
For example: NL123456789B01
Here is what each part means:
- NL — the country prefix (Netherlands)
- 9 digits — derived from the company’s RSIN (a government identification number)
- B — a fixed separator that stands for “BTW”
- 2 digits — a sequence number, usually
01for the main entity
A single legal entity can have multiple BTW numbers if it operates separate divisions, in which case the final two digits increment (B02, B03, etc.). This is uncommon but worth knowing.
Format Validation
Before making any API call, you can do a quick format check:
function isValidBtwFormat(vatNumber) {
const pattern = /^NL\d{9}B\d{2}$/;
return pattern.test(vatNumber.replace(/\s/g, '').toUpperCase());
}
// Examples
isValidBtwFormat('NL123456789B01'); // true
isValidBtwFormat('NL 1234 5678 9B01'); // true (spaces stripped)
isValidBtwFormat('DE123456789'); // false (wrong country)
isValidBtwFormat('NL12345678B01'); // false (only 8 digits)
Format validation catches obvious errors immediately, without an API round trip. But it does not tell you whether the number is actually registered and active. For that, you need VIES.
What Is VIES?
VIES (VAT Information Exchange System) is a service operated by the European Commission that lets you verify whether a VAT number is valid and currently registered. It covers all EU member states, including the Netherlands.
When you query VIES with a Dutch BTW number, it tells you:
- Whether the number is valid (registered and active)
- The company name associated with the number
- The registered address
VIES is the authoritative source. If VIES says a number is valid, you can apply the reverse charge mechanism with confidence.
The Problem with Using VIES Directly
The official VIES service has some practical limitations:
- Reliability — the VIES SOAP endpoint experiences regular downtime, especially during tax filing periods
- Speed — response times vary widely, from sub-second to several seconds
- Rate limiting — heavy usage can get your IP blocked without warning
- SOAP interface — the official API uses SOAP/XML, which is cumbersome to work with in modern applications
These issues make VIES unreliable as a real-time validation step in checkout or onboarding flows.
Validating via the KVKBase API
KVKBase provides a REST endpoint that wraps VIES with caching, retry logic, and a clean JSON interface. This gives you reliable validation without dealing with SOAP or VIES downtime:
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.kvkbase.nl/api/v1/vat/NL123456789B01"
Response:
{
"valid": true,
"vatNumber": "NL123456789B01",
"tradeName": "Voorbeeld BV",
"address": "Herengracht 500, 1017CB Amsterdam",
"countryCode": "NL"
}
If the number is invalid or deregistered:
{
"valid": false,
"vatNumber": "NL000000000B00",
"tradeName": null,
"address": null,
"countryCode": "NL"
}
Integration Patterns
Invoicing Workflow
The most common use case is validating a customer’s VAT number before issuing a B2B invoice. If the customer has a valid EU VAT number and is in a different member state, you apply the reverse charge mechanism (no VAT charged). If the number is invalid, you charge the standard Dutch rate (21%).
import requests
API_KEY = "your-api-key"
BASE_URL = "https://api.kvkbase.nl/api/v1"
def validate_vat(vat_number):
response = requests.get(
f"{BASE_URL}/vat/{vat_number}",
headers={"Authorization": f"Bearer {API_KEY}"}
)
response.raise_for_status()
return response.json()
def calculate_vat(amount, customer_vat, seller_country="NL"):
if not customer_vat:
# B2C: always charge VAT
return amount * 0.21
result = validate_vat(customer_vat)
if not result["valid"]:
raise ValueError(f"Invalid VAT number: {customer_vat}")
if result["countryCode"] != seller_country:
# Reverse charge: valid EU VAT, different country
return 0.0
# Same country: charge Dutch VAT
return amount * 0.21
Customer Onboarding
During sign-up, validate the VAT number and use it to pull additional company data:
async function onboardCustomer(vatNumber) {
// Step 1: Validate VAT
const vatResult = await fetch(
`${BASE_URL}/vat/${vatNumber}`,
{ headers: { 'Authorization': `Bearer ${API_KEY}` } }
).then(r => r.json());
if (!vatResult.valid) {
return { error: 'Invalid VAT number. Please check and try again.' };
}
// Step 2: If Dutch, also look up KVK data for full profile
if (vatResult.countryCode === 'NL') {
const kvkResult = await fetch(
`${BASE_URL}/search?q=${encodeURIComponent(vatResult.tradeName)}&limit=1`,
{ headers: { 'Authorization': `Bearer ${API_KEY}` } }
).then(r => r.json());
return {
vatValid: true,
companyName: vatResult.tradeName,
address: vatResult.address,
kvkNumber: kvkResult[0]?.kvkNumber || null
};
}
return {
vatValid: true,
companyName: vatResult.tradeName,
address: vatResult.address
};
}
Batch Validation
If you need to validate a list of VAT numbers (for example, when cleaning up your customer database), loop through them with some rate limiting:
async function validateBatch(vatNumbers) {
const results = [];
for (const vat of vatNumbers) {
try {
const result = await fetch(
`${BASE_URL}/vat/${vat}`,
{ headers: { 'Authorization': `Bearer ${API_KEY}` } }
).then(r => r.json());
results.push({ vat, ...result });
} catch (err) {
results.push({ vat, valid: false, error: err.message });
}
// Respect rate limits
await new Promise(resolve => setTimeout(resolve, 100));
}
return results;
}
Common Pitfalls
A few things that trip up developers working with Dutch VAT numbers:
- Spaces in the number. Users often enter
NL 1234 5678 9 B01. Strip all whitespace before validation. - Lowercase input.
nl123456789b01is the same number. Normalize to uppercase. - Confusing KVK and BTW numbers. A KVK number is 8 digits. A BTW number starts with
NLand is longer. They are different identifiers for the same company. - Stale validation. A VAT number that was valid last month might be deregistered today. Re-validate periodically if you store the result.
- VIES downtime. Always handle the case where validation is temporarily unavailable. Queue and retry rather than blocking the user.
Summary
Dutch VAT validation is a necessary part of any B2B application that operates in the EU. Start with format validation to catch typos, then use the KVKBase API for authoritative VIES checks without the hassle of SOAP or reliability issues. Combine VAT validation with KVK lookups for a complete picture of your Dutch business customers.
Get started with a free API key at kvkbase.nl. The API reference documents all VAT and KVK endpoints.