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.
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 field | Exact Online field | Entity | Note |
|---|---|---|---|
| tradeName | Name | Account | Trade name as primary name |
| kvkNumber | ChamberOfCommerce | Account | KVK number field |
| vatNumber | VATNumber | Account | VAT number |
| address.street + houseNumber | AddressLine1 | Address | Combined street + number |
| address.postalCode | Postcode | Address | Postal code |
| address.city | City | Address | City |
| legalForm | Custom field | Account | Via custom field |
| sbiCodes | Custom field | Account | Via 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 field | AFAS field | Connector | Note |
|---|---|---|---|
| tradeName | Nm | KnOrganisation | Name |
| kvkNumber | CcNr | KnOrganisation | KVK number |
| vatNumber | VaId | KnOrganisation | VAT number |
| address.street | Ad | KnBasicAddress | Street |
| address.houseNumber | HmNr | KnBasicAddress | House number |
| address.postalCode | ZpCd | KnBasicAddress | Postal code |
| address.city | Rs | KnBasicAddress | City |
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.