ERP Integratie met KVK Data: Exact Online, AFAS en meer
· KVKBase Team

ERP Integratie met KVK Data: Exact Online, AFAS en meer

Gids voor het koppelen van KVK-data aan ERP-systemen als Exact Online en AFAS. Veldmapping, automatische klantenregistratie en synchronisatiestrategieen.

erpintegratieexact-online

ERP Integratie met KVK Data: Exact Online, AFAS en meer

Het handmatig invoeren van klantgegevens in je ERP-systeem is foutgevoelig, tijdrovend en volkomen overbodig. Door KVK-data direct te koppelen aan je ERP, automatiseer je de aanmaak van relaties en houd je stamgegevens actueel.

In dit artikel laten we zien hoe je KVK-data integreert met populaire Nederlandse ERP-systemen: Exact Online, AFAS en vergelijkbare pakketten.

Waarom ERP-integratie met KVK-data?

De voordelen zijn direct merkbaar:

  • Geen typefouten: bedrijfsnamen en adressen komen rechtstreeks uit het handelsregister
  • Snellere klantregistratie: een nieuw contact aanmaken duurt seconden in plaats van minuten
  • Actuele data: periodieke synchronisatie houdt stamgegevens up-to-date
  • Compliance: correcte BTW-nummers en KVK-registraties op facturen
  • Minder duplicaten: herken bestaande relaties op basis van KVK-nummer

Exact Online integratie

Exact Online is het meest gebruikte cloud-ERP-systeem in Nederland. De integratie verloopt via de Exact Online REST API.

Authenticatie

Exact Online gebruikt OAuth 2.0. Je hebt een app-registratie nodig in het 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
});

Veldmapping: KVK naar Exact Online Relatie

KVK-veldExact Online veldEntiteitOpmerking
tradeNameNameAccountHandelsnaam als primaire naam
kvkNumberChamberOfCommerceAccountKVK-nummer veld
vatNumberVATNumberAccountBTW-nummer
address.street + houseNumberAddressLine1AddressGecombineerd straat + nummer
address.postalCodePostcodeAddressPostcode
address.cityCityAddressPlaats
legalFormCustom fieldAccountVia eigen veld
sbiCodesCustom fieldAccountVia eigen veld

Nieuwe relatie aanmaken

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

  if (!kvkData || !kvkData.isActive) {
    throw new Error('Bedrijf niet gevonden of niet actief');
  }

  // Controleer of relatie al bestaat
  const existing = await client.get('crm/Accounts', {
    $filter: `ChamberOfCommerce eq '${kvkData.kvkNumber}'`
  });

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

  // Maak nieuwe relatie aan
  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 };
}

Bestaande relaties updaten

async function syncExactRelations() {
  // Haal alle relaties op met een KVK-nummer
  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;

      // Vergelijk en update alleen als er verschillen zijn
      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 fout voor ${account.ChamberOfCommerce}:`, error);
      errors++;
    }

    await sleep(200); // Rate limit respecteren
  }

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

AFAS integratie

AFAS Profit is een ander veelgebruikt ERP-systeem in Nederland. De integratie verloopt via AFAS GetConnectors en UpdateConnectors.

Authenticatie

AFAS gebruikt een token-gebaseerd systeem:

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'
};

Veldmapping: KVK naar AFAS

KVK-veldAFAS-veldConnectorOpmerking
tradeNameNmKnOrganisationNaam
kvkNumberCcNrKnOrganisationKVK-nummer
vatNumberVaIdKnOrganisationBTW-nummer
address.streetAdKnBasicAddressStraat
address.houseNumberHmNrKnBasicAddressHuisnummer
address.postalCodeZpCdKnBasicAddressPostcode
address.cityRsKnBasicAddressWoonplaats

Nieuwe organisatie aanmaken in AFAS

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

  if (!kvkData) {
    throw new Error('Bedrijf niet gevonden');
  }

  const payload = {
    KnOrganisation: {
      Element: {
        Fields: {
          MatchOga: 0, // Nieuwe organisatie
          Nm: kvkData.tradeName,
          CcNr: kvkData.kvkNumber,
          VaId: kvkData.vatNumber || '',
          PbAd: true // Postbusadres = false, bezoekadres = 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 fout: ${error}`);
  }

  return response.json();
}

Generiek integratie-patroon

Ongeacht welk ERP-systeem je gebruikt, volgt de integratie hetzelfde patroon:

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

  async createFromKvk(kvkNumber) {
    // 1. Haal KVK-data op
    const company = await this.kvk.lookup(kvkNumber);
    if (!company) throw new Error('Bedrijf niet gevonden');

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

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

    // 4. Maak aan 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

// Implementeer dit voor elk ERP-systeem
class ErpAdapter {
  async findByKvk(kvkNumber) { /* zoek relatie op KVK-nummer */ }
  async create(data) { /* maak nieuwe relatie aan */ }
  async update(id, data) { /* update bestaande relatie */ }
  async getAllWithKvk() { /* haal alle relaties met KVK op */ }
  mapFromKvk(kvkData) { /* map KVK-velden naar ERP-velden */ }
  detectChanges(existing, fresh) { /* vergelijk bestaand met vers */ }
}

Best practices

1. Gebruik KVK-nummer als koppelsleutel

Het KVK-nummer is uniek en verandert nooit. Gebruik het als de stabiele identifier om records te koppelen tussen systemen. Sla het altijd op als CHAR(8) om voorloopnullen te behouden.

2. Synchroniseer in een richting

Houd het simpel: KVK-data is de bron van waarheid voor bedrijfsstamgegevens. Laat wijzigingen vanuit de KVK-bron naar het ERP stromen, niet andersom.

3. Log alle wijzigingen

Houd een audit trail bij van wat er wanneer is gewijzigd:

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

4. Plan synchronisatie buiten kantooruren

ERP-systemen hebben vaak beperkte API-capaciteit. Plan je batch-synchronisatie in de nacht of vroege ochtend wanneer het systeem minder belast is.

5. Handel fouten robuust af

Een falende synchronisatie voor een relatie mag niet de hele batch stoppen. Log de fout, ga door met de volgende en probeer gefaalde records later opnieuw.

Conclusie

ERP-integratie met KVK-data bespaart uren handmatig werk en voorkomt fouten in je stamgegevens. Het patroon is voor elk ERP-systeem hetzelfde: ophalen, mappen, aanmaken of updaten.

Met KVKBase als databron heb je een enkele API die je alle bedrijfsgegevens levert die je nodig hebt — van handelsnaam en adres tot KVK-nummer en BTW-identificatie. Combineer dat met de adapter-patronen uit dit artikel en je hebt een robuuste koppeling tussen je KVK-data en je ERP, ongeacht welk systeem je gebruikt.