Intégration ERP et données KVK : Exact Online, AFAS et autres
Guide pour connecter les données KVK à des ERP comme Exact Online et AFAS : correspondance des champs, création automatique de clients, synchronisation.
Saisir à la main les données clients dans votre ERP est source d’erreurs, chronophage et totalement inutile. En connectant directement les données KVK à votre ERP, vous automatisez la création des fiches tiers et maintenez vos données de référence à jour.
Dans cet article, nous montrons comment intégrer les données KVK aux ERP néerlandais les plus courants : Exact Online, AFAS et progiciels similaires.
Pourquoi intégrer votre ERP aux données KVK ?
Les avantages se font sentir immédiatement :
- Zéro faute de frappe : les raisons sociales et les adresses proviennent directement du registre du commerce
- Création client plus rapide : créer un nouveau contact prend quelques secondes au lieu de plusieurs minutes
- Données à jour : une synchronisation périodique maintient les données de référence à jour
- Conformité : des numéros de TVA et des inscriptions KVK corrects sur les factures
- Moins de doublons : reconnaissez les tiers existants grâce au numéro KVK
Intégration Exact Online
Exact Online est l’ERP cloud le plus utilisé aux Pays-Bas. L’intégration passe par l’API REST d’Exact Online.
Authentification
Exact Online utilise OAuth 2.0. Vous devez enregistrer une application dans l’App Center d’Exact Online :
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
});
Correspondance des champs : KVK vers fiche tiers Exact Online
| Champ KVK | Champ Exact Online | Entité | Remarque |
|---|---|---|---|
| tradeName | Name | Account | Nom commercial comme nom principal |
| kvkNumber | ChamberOfCommerce | Account | Champ du numéro KVK |
| vatNumber | VATNumber | Account | Numéro de TVA |
| address.street + houseNumber | AddressLine1 | Address | Rue + numéro combinés |
| address.postalCode | Postcode | Address | Code postal |
| address.city | City | Address | Ville |
| legalForm | Champ personnalisé | Account | Via un champ personnalisé |
| sbiCodes | Champ personnalisé | Account | Via un champ personnalisé |
Création d’une nouvelle fiche tiers
async function createExactRelation(kvkNumber) {
// Récupérer les données KVK
const kvkData = await kvkbase.lookup(kvkNumber);
if (!kvkData || !kvkData.isActive) {
throw new Error('Company not found or not active');
}
// Vérifier si la fiche existe déjà
const existing = await client.get('crm/Accounts', {
$filter: `ChamberOfCommerce eq '${kvkData.kvkNumber}'`
});
if (existing.length > 0) {
return { action: 'exists', account: existing[0] };
}
// Créer la nouvelle fiche
const account = await client.post('crm/Accounts', {
Name: kvkData.tradeName,
ChamberOfCommerce: kvkData.kvkNumber,
VATNumber: kvkData.vatNumber || '',
Status: 'C', // Client
AddressLine1: `${kvkData.address.street} ${kvkData.address.houseNumber}`,
Postcode: kvkData.address.postalCode,
City: kvkData.address.city,
Country: 'NL'
});
return { action: 'created', account };
}
Mise à jour des fiches existantes
async function syncExactRelations() {
// Récupérer toutes les fiches ayant un numéro KVK
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;
// Comparer et ne mettre à jour qu'en cas de différences
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); // Respecter les limites de débit
}
return { total: accounts.length, updated, errors };
}
Intégration AFAS
AFAS Profit est un autre ERP très répandu aux Pays-Bas. L’intégration passe par les GetConnectors et UpdateConnectors d’AFAS.
Authentification
AFAS utilise un système à base de jeton :
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'
};
Correspondance des champs : KVK vers AFAS
| Champ KVK | Champ AFAS | Connecteur | Remarque |
|---|---|---|---|
| tradeName | Nm | KnOrganisation | Nom |
| kvkNumber | CcNr | KnOrganisation | Numéro KVK |
| vatNumber | VaId | KnOrganisation | Numéro de TVA |
| address.street | Ad | KnBasicAddress | Rue |
| address.houseNumber | HmNr | KnBasicAddress | Numéro |
| address.postalCode | ZpCd | KnBasicAddress | Code postal |
| address.city | Rs | KnBasicAddress | Ville |
Création d’une nouvelle organisation dans 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, // Nouvelle organisation
Nm: kvkData.tradeName,
CcNr: kvkData.kvkNumber,
VaId: kvkData.vatNumber || '',
PbAd: true // Adresse postale = false, adresse de visite = 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();
}
Schéma d’intégration générique
Quel que soit l’ERP que vous utilisez, l’intégration suit le même schéma :
class ErpKvkSync {
constructor(kvkClient, erpAdapter) {
this.kvk = kvkClient;
this.erp = erpAdapter;
}
async createFromKvk(kvkNumber) {
// 1. Récupérer les données KVK
const company = await this.kvk.lookup(kvkNumber);
if (!company) throw new Error('Company not found');
// 2. Vérifier si elle existe déjà
const existing = await this.erp.findByKvk(kvkNumber);
if (existing) return { action: 'exists', id: existing.id };
// 3. Faire correspondre les champs
const mapped = this.erp.mapFromKvk(company);
// 4. Créer dans l'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;
}
}
Interface d’adaptateur ERP
// À implémenter pour chaque ERP
class ErpAdapter {
async findByKvk(kvkNumber) { /* trouver une fiche par numéro KVK */ }
async create(data) { /* créer une nouvelle fiche */ }
async update(id, data) { /* mettre à jour une fiche existante */ }
async getAllWithKvk() { /* récupérer toutes les fiches avec KVK */ }
mapFromKvk(kvkData) { /* faire correspondre les champs KVK aux champs ERP */ }
detectChanges(existing, fresh) { /* comparer l'existant avec les données récentes */ }
}
Bonnes pratiques
1. Utilisez le numéro KVK comme clé de liaison
Le numéro KVK est unique et ne change jamais. Utilisez-le comme identifiant stable pour relier les enregistrements entre systèmes. Stockez-le toujours en CHAR(8) pour conserver les zéros initiaux.
2. Synchronisez dans un seul sens
Restez simple : les données KVK sont la source de vérité pour les données de référence des entreprises. Laissez les modifications aller de la source KVK vers l’ERP, et non l’inverse.
3. Journalisez toutes les modifications
Conservez une piste d’audit de ce qui a changé et quand :
async function logSync(kvkNumber, erpId, changes) {
await db.syncLog.create({
kvkNumber,
erpId,
changes: JSON.stringify(changes),
syncedAt: new Date()
});
}
4. Planifiez la synchronisation en dehors des heures de bureau
Les ERP ont souvent une capacité d’API limitée. Planifiez votre synchronisation par lots la nuit ou tôt le matin, lorsque le système est moins sollicité.
5. Gérez les erreurs avec souplesse
L’échec de la synchronisation d’une fiche ne doit pas interrompre tout le lot. Journalisez l’erreur, passez à l’enregistrement suivant et retentez plus tard les enregistrements en échec.
Conclusion
L’intégration ERP avec les données KVK fait gagner des heures de travail manuel et évite les erreurs dans vos données de référence. Le schéma est le même pour chaque ERP : récupérer, faire correspondre, créer ou mettre à jour.
Avec KVKBase comme source de données, vous disposez d’une API unique qui fournit toutes les informations d’entreprise dont vous avez besoin, du nom commercial et de l’adresse au numéro KVK et à l’identification TVA. Associez-la aux schémas d’adaptateur de cet article et vous obtenez une connexion robuste entre vos données KVK et votre ERP, quel que soit le système.