ERP Integration with KVK Data: Exact Online, AFAS, and More
KVKBase Team

ERP Integration with KVK Data: Exact Online, AFAS, and More

Guide to connecting KVK data to ERP systems like Exact Online and AFAS. Field mapping, automated customer creation, and synchronization strategies.

erpintegrationexact-online

ERP Integration with KVK Data: Exact Online, AFAS, and More

Manually entering customer data into your ERP system is error-prone, time-consuming, and entirely unnecessary. By connecting KVK data directly to your ERP, you automate the creation of relations and keep master data up to date.

In this article we show how to integrate KVK data with popular Dutch ERP systems: Exact Online, AFAS, and similar packages.

Why integrate your ERP with KVK data?

The benefits are immediately noticeable:

  • No typos: company names and addresses come straight from the trade register
  • Faster customer registration: creating a new contact takes seconds instead of minutes
  • Current data: periodic synchronization keeps master data up to date
  • Compliance: correct VAT numbers and KVK registrations on invoices
  • Fewer duplicates: recognize existing relations based on KVK number

Exact Online integration

Exact Online is the most widely used cloud ERP system in the Netherlands. The integration runs through the Exact Online REST API.

Authentication

Exact Online uses OAuth 2.0. You need an app registration in the Exact Online App Center:

const exact = require('exact-online-client');

const client = new exact.Client({
  clientId: process.env.EXACT_CLIENT_ID,
  clientSecret: process.env.EXACT_CLIENT_SECRET,
  redirectUri: process.env.EXACT_REDIRECT_URI,
  refreshToken: process.env.EXACT_REFRESH_TOKEN
});

Field mapping: KVK to Exact Online relation

KVK fieldExact Online fieldEntityNote
tradeNameNameAccountTrade name as primary name
kvkNumberChamberOfCommerceAccountKVK number field
vatNumberVATNumberAccountVAT number
address.street + houseNumberAddressLine1AddressCombined street + number
address.postalCodePostcodeAddressPostal code
address.cityCityAddressCity
legalFormCustom fieldAccountVia custom field
sbiCodesCustom fieldAccountVia custom field

Creating a new relation

async function createExactRelation(kvkNumber) {
  // Fetch KVK data
  const kvkData = await kvkbase.lookup(kvkNumber);

  if (!kvkData || !kvkData.isActive) {
    throw new Error('Company not found or not active');
  }

  // Check if the relation already exists
  const existing = await client.get('crm/Accounts', {
    $filter: `ChamberOfCommerce eq '${kvkData.kvkNumber}'`
  });

  if (existing.length > 0) {
    return { action: 'exists', account: existing[0] };
  }

  // Create new relation
  const account = await client.post('crm/Accounts', {
    Name: kvkData.tradeName,
    ChamberOfCommerce: kvkData.kvkNumber,
    VATNumber: kvkData.vatNumber || '',
    Status: 'C', // Customer
    AddressLine1: `${kvkData.address.street} ${kvkData.address.houseNumber}`,
    Postcode: kvkData.address.postalCode,
    City: kvkData.address.city,
    Country: 'NL'
  });

  return { action: 'created', account };
}

Updating existing relations

async function syncExactRelations() {
  // Fetch all relations with a KVK number
  const accounts = await client.get('crm/Accounts', {
    $filter: "ChamberOfCommerce ne ''",
    $select: 'ID,Name,ChamberOfCommerce,VATNumber,AddressLine1,Postcode,City'
  });

  let updated = 0;
  let errors = 0;

  for (const account of accounts) {
    try {
      const kvkData = await kvkbase.lookup(account.ChamberOfCommerce);

      if (!kvkData) continue;

      // Compare and update only if there are differences
      const updates = {};

      if (account.Name !== kvkData.tradeName) {
        updates.Name = kvkData.tradeName;
      }

      const expectedAddress = `${kvkData.address.street} ${kvkData.address.houseNumber}`;
      if (account.AddressLine1 !== expectedAddress) {
        updates.AddressLine1 = expectedAddress;
      }

      if (account.Postcode !== kvkData.address.postalCode) {
        updates.Postcode = kvkData.address.postalCode;
      }

      if (Object.keys(updates).length > 0) {
        await client.put(`crm/Accounts(guid'${account.ID}')`, updates);
        updated++;
      }
    } catch (error) {
      console.error(`Sync error for ${account.ChamberOfCommerce}:`, error);
      errors++;
    }

    await sleep(200); // Respect rate limits
  }

  return { total: accounts.length, updated, errors };
}

AFAS integration

AFAS Profit is another widely used ERP system in the Netherlands. The integration runs through AFAS GetConnectors and UpdateConnectors.

Authentication

AFAS uses a token-based system:

const afasBaseUrl = `https://${process.env.AFAS_ENV_ID}.rest.afas.online/profitrestservices`;
const afasHeaders = {
  'Authorization': `AfasToken ${Buffer.from(JSON.stringify({
    token: process.env.AFAS_TOKEN
  })).toString('base64')}`,
  'Content-Type': 'application/json'
};

Field mapping: KVK to AFAS

KVK fieldAFAS fieldConnectorNote
tradeNameNmKnOrganisationName
kvkNumberCcNrKnOrganisationKVK number
vatNumberVaIdKnOrganisationVAT number
address.streetAdKnBasicAddressStreet
address.houseNumberHmNrKnBasicAddressHouse number
address.postalCodeZpCdKnBasicAddressPostal code
address.cityRsKnBasicAddressCity

Creating a new organization in AFAS

async function createAfasOrganisation(kvkNumber) {
  const kvkData = await kvkbase.lookup(kvkNumber);

  if (!kvkData) {
    throw new Error('Company not found');
  }

  const payload = {
    KnOrganisation: {
      Element: {
        Fields: {
          MatchOga: 0, // New organization
          Nm: kvkData.tradeName,
          CcNr: kvkData.kvkNumber,
          VaId: kvkData.vatNumber || '',
          PbAd: true // Postal address = false, visiting address = true
        },
        Objects: {
          KnBasicAddressAdr: {
            Element: {
              Fields: {
                Ad: kvkData.address.street,
                HmNr: parseInt(kvkData.address.houseNumber) || 0,
                ZpCd: kvkData.address.postalCode,
                Rs: kvkData.address.city,
                CoId: 'NL'
              }
            }
          }
        }
      }
    }
  };

  const response = await fetch(`${afasBaseUrl}/connectors/KnOrganisation`, {
    method: 'POST',
    headers: afasHeaders,
    body: JSON.stringify(payload)
  });

  if (!response.ok) {
    const error = await response.text();
    throw new Error(`AFAS error: ${error}`);
  }

  return response.json();
}

Generic integration pattern

Regardless of which ERP system you use, the integration follows the same pattern:

class ErpKvkSync {
  constructor(kvkClient, erpAdapter) {
    this.kvk = kvkClient;
    this.erp = erpAdapter;
  }

  async createFromKvk(kvkNumber) {
    // 1. Fetch KVK data
    const company = await this.kvk.lookup(kvkNumber);
    if (!company) throw new Error('Company not found');

    // 2. Check if it already exists
    const existing = await this.erp.findByKvk(kvkNumber);
    if (existing) return { action: 'exists', id: existing.id };

    // 3. Map fields
    const mapped = this.erp.mapFromKvk(company);

    // 4. Create in ERP
    const created = await this.erp.create(mapped);
    return { action: 'created', id: created.id };
  }

  async syncAll() {
    const relations = await this.erp.getAllWithKvk();
    const results = { updated: 0, skipped: 0, errors: 0 };

    for (const relation of relations) {
      try {
        const fresh = await this.kvk.lookup(relation.kvkNumber);
        if (!fresh) { results.skipped++; continue; }

        const mapped = this.erp.mapFromKvk(fresh);
        const hasChanges = this.erp.detectChanges(relation, mapped);

        if (hasChanges) {
          await this.erp.update(relation.id, mapped);
          results.updated++;
        } else {
          results.skipped++;
        }
      } catch {
        results.errors++;
      }

      await sleep(200);
    }

    return results;
  }
}

ERP Adapter interface

// Implement this for each ERP system
class ErpAdapter {
  async findByKvk(kvkNumber) { /* find relation by KVK number */ }
  async create(data) { /* create new relation */ }
  async update(id, data) { /* update existing relation */ }
  async getAllWithKvk() { /* fetch all relations with KVK */ }
  mapFromKvk(kvkData) { /* map KVK fields to ERP fields */ }
  detectChanges(existing, fresh) { /* compare existing with fresh */ }
}

Best practices

1. Use the KVK number as the linking key

The KVK number is unique and never changes. Use it as the stable identifier to link records between systems. Always store it as CHAR(8) to preserve leading zeros.

2. Synchronize in one direction

Keep it simple: KVK data is the source of truth for company master data. Let changes flow from the KVK source to the ERP, not the other way around.

3. Log all changes

Maintain an audit trail of what changed and when:

async function logSync(kvkNumber, erpId, changes) {
  await db.syncLog.create({
    kvkNumber,
    erpId,
    changes: JSON.stringify(changes),
    syncedAt: new Date()
  });
}

4. Schedule synchronization outside business hours

ERP systems often have limited API capacity. Plan your batch synchronization at night or early morning when the system is under less load.

5. Handle errors gracefully

A failing synchronization for one relation should not stop the entire batch. Log the error, continue with the next record, and retry failed records later.

Conclusion

ERP integration with KVK data saves hours of manual work and prevents errors in your master data. The pattern is the same for every ERP system: fetch, map, create or update.

With KVKBase as your data source, you have a single API that delivers all the company details you need — from trade name and address to KVK number and VAT identification. Combine that with the adapter patterns from this article and you have a robust connection between your KVK data and your ERP, regardless of which system you use.