KVKBase

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.

vatbtwviesvalidationcompliance

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 01 for 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. nl123456789b01 is the same number. Normalize to uppercase.
  • Confusing KVK and BTW numbers. A KVK number is 8 digits. A BTW number starts with NL and 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.