Intégration ERP et données KVK : Exact Online, AFAS et autres
KVKBase Team

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.

erpintégrationexact-online

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 KVKChamp Exact OnlineEntitéRemarque
tradeNameNameAccountNom commercial comme nom principal
kvkNumberChamberOfCommerceAccountChamp du numéro KVK
vatNumberVATNumberAccountNuméro de TVA
address.street + houseNumberAddressLine1AddressRue + numéro combinés
address.postalCodePostcodeAddressCode postal
address.cityCityAddressVille
legalFormChamp personnaliséAccountVia un champ personnalisé
sbiCodesChamp personnaliséAccountVia 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 KVKChamp AFASConnecteurRemarque
tradeNameNmKnOrganisationNom
kvkNumberCcNrKnOrganisationNuméro KVK
vatNumberVaIdKnOrganisationNuméro de TVA
address.streetAdKnBasicAddressRue
address.houseNumberHmNrKnBasicAddressNuméro
address.postalCodeZpCdKnBasicAddressCode postal
address.cityRsKnBasicAddressVille

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.