How to Integrate a Dutch Company Data API in Your SaaS
· KVKBase Team

How to Integrate a Dutch Company Data API in Your SaaS

Step-by-step guide for integrating KVK company data into your SaaS application, with code examples, error handling, and caching best practices.

apiintegrationsaas

How to Integrate a Dutch Company Data API in Your SaaS

If your SaaS serves Dutch businesses, you will eventually need access to company data from the KVK (Kamer van Koophandel) — the Dutch Chamber of Commerce. Whether it is for customer onboarding, invoice generation, or compliance checks, reliable access to KVK data is a building block for any serious B2B application.

This guide walks you through integrating a Dutch company data API from scratch, with practical code examples and production-ready patterns.

Why You Need Dutch Company Data

Dutch businesses are identified by an 8-digit KVK number. This number unlocks a wealth of publicly available information: legal name, trade names, registered address, legal form (BV, NV, eenmanszaak), SBI activity codes, and more.

Common use cases include:

  • Customer onboarding: auto-fill company details from a KVK number
  • Invoice compliance: ensure correct legal names and addresses
  • Fraud prevention: verify that a company actually exists and is active
  • Data enrichment: keep your CRM or database current

Step 1: Get Your API Key

First, sign up for a KVKBase account at kvkbase.nl. After registration, you will receive an API key that authenticates your requests.

Store your API key securely. Never commit it to version control or expose it in client-side code.

# Store in environment variable
export KVKBASE_API_KEY="your-api-key-here"

Step 2: Make Your First Request

The simplest operation is a lookup by KVK number. Here is how to do it with curl:

curl -s -H "Authorization: Bearer $KVKBASE_API_KEY" \
  "https://api.kvkbase.nl/api/v1/lookup/12345678" | jq

And in JavaScript:

const KVKBASE_API_KEY = process.env.KVKBASE_API_KEY;
const BASE_URL = 'https://api.kvkbase.nl/api/v1';

async function lookupCompany(kvkNumber) {
  const response = await fetch(`${BASE_URL}/lookup/${kvkNumber}`, {
    headers: {
      'Authorization': `Bearer ${KVKBASE_API_KEY}`,
      'Content-Type': 'application/json'
    }
  });

  if (!response.ok) {
    throw new Error(`API error: ${response.status} ${response.statusText}`);
  }

  return response.json();
}

Step 3: Handle the Response

A typical response includes the following fields:

{
  "kvkNumber": "12345678",
  "tradeName": "Example BV",
  "legalForm": "Besloten Vennootschap",
  "address": {
    "street": "Voorbeeldstraat",
    "houseNumber": "1",
    "postalCode": "1234AB",
    "city": "Amsterdam"
  },
  "sbiCodes": [
    { "code": "6201", "description": "Ontwikkelen en produceren van software" }
  ],
  "isActive": true
}

Map these fields to your internal data model. Be explicit about which fields you need and handle missing fields gracefully — not every company has every field populated.

function mapToInternalModel(apiResponse) {
  return {
    companyId: apiResponse.kvkNumber,
    name: apiResponse.tradeName,
    legalForm: apiResponse.legalForm,
    fullAddress: formatAddress(apiResponse.address),
    industryCodes: apiResponse.sbiCodes?.map(s => s.code) || [],
    active: apiResponse.isActive
  };
}

function formatAddress(addr) {
  if (!addr) return null;
  return `${addr.street} ${addr.houseNumber}, ${addr.postalCode} ${addr.city}`;
}

Step 4: Implement Error Handling

A production integration must handle several error scenarios:

async function lookupCompanySafe(kvkNumber) {
  try {
    const response = await fetch(`${BASE_URL}/lookup/${kvkNumber}`, {
      headers: { 'Authorization': `Bearer ${KVKBASE_API_KEY}` }
    });

    switch (response.status) {
      case 200:
        return { success: true, data: await response.json() };
      case 404:
        return { success: false, error: 'Company not found' };
      case 401:
        return { success: false, error: 'Invalid API key' };
      case 429:
        const retryAfter = response.headers.get('Retry-After') || '60';
        return { success: false, error: 'Rate limited', retryAfter: parseInt(retryAfter) };
      default:
        return { success: false, error: `Unexpected error: ${response.status}` };
    }
  } catch (error) {
    return { success: false, error: `Network error: ${error.message}` };
  }
}

Step 5: Add Caching

Company data does not change frequently. Caching reduces your API usage, improves response times, and makes your application more resilient to API downtime.

import NodeCache from 'node-cache';

// TTL: 24 hours for active companies
const companyCache = new NodeCache({ stdTTL: 86400 });

async function lookupWithCache(kvkNumber) {
  const cached = companyCache.get(kvkNumber);
  if (cached) return cached;

  const result = await lookupCompanySafe(kvkNumber);
  if (result.success) {
    companyCache.set(kvkNumber, result);
  }

  return result;
}

For production systems, consider using Redis instead of an in-memory cache, especially if you run multiple server instances.

Step 6: Respect Rate Limits

Every API has limits. Handle rate limiting gracefully with exponential backoff:

async function lookupWithRetry(kvkNumber, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const result = await lookupCompanySafe(kvkNumber);

    if (result.error === 'Rate limited') {
      const delay = result.retryAfter
        ? result.retryAfter * 1000
        : Math.pow(2, attempt) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
      continue;
    }

    return result;
  }

  return { success: false, error: 'Max retries exceeded' };
}

Step 7: Add Search Functionality

Beyond direct lookups, you often need to search for companies by name:

async function searchCompanies(query, limit = 10) {
  const params = new URLSearchParams({ q: query, limit: limit.toString() });
  const response = await fetch(`${BASE_URL}/search?${params}`, {
    headers: { 'Authorization': `Bearer ${KVKBASE_API_KEY}` }
  });

  if (!response.ok) {
    throw new Error(`Search failed: ${response.status}`);
  }

  return response.json();
}

// Usage
const results = await searchCompanies('Bol.com');

Putting It All Together

Here is a complete service class that wraps all the functionality:

class KvkService {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.baseUrl = 'https://api.kvkbase.nl/api/v1';
    this.cache = new NodeCache({ stdTTL: 86400 });
  }

  async lookup(kvkNumber) {
    return this.withCache(`lookup:${kvkNumber}`, () =>
      this.request(`/lookup/${kvkNumber}`)
    );
  }

  async search(query, limit = 10) {
    return this.request(`/search?q=${encodeURIComponent(query)}&limit=${limit}`);
  }

  async withCache(key, fn) {
    const cached = this.cache.get(key);
    if (cached) return cached;
    const result = await fn();
    this.cache.set(key, result);
    return result;
  }

  async request(path) {
    const response = await fetch(`${this.baseUrl}${path}`, {
      headers: { 'Authorization': `Bearer ${this.apiKey}` }
    });
    if (!response.ok) throw new Error(`API error: ${response.status}`);
    return response.json();
  }
}

Next Steps

Once you have the basic integration working, consider:

  • Adding the KVKBase widget to your frontend for instant company lookups
  • Setting up periodic re-validation of stored company data
  • Combining KVK lookups with BTW/VAT validation for complete B2B compliance

With KVKBase, you get a single API that covers KVK lookups, company search, and data validation — everything you need to build a solid Dutch B2B integration.