KVKBase Team

Vérifier des entreprises belges en masse avec l'API par lots

Contrôler un fichier CSV de numéros d'entreprise avec l'API par lots : normalisation, clé de contrôle, introuvables, inactives, script Python et cache.

belgiqueapibatchqualité des données

Vous avez en main l’export de votre logiciel de facturation : mille clients, mille numéros d’entreprise, et une question simple : lesquels sont encore bons ? Les interroger un par un fonctionne, mais l’endpoint par lots de KVKBase est plus économe en code : jusqu’à 25 numéros par appel, avec un résultat distinct pour chacun. Voici comment traiter un fichier CSV de bout en bout, du nettoyage des numéros au script Python, en passant par le cache. Les données sont les données officielles d’entreprises belges, accessibles via l’API.

Fonctionnement de l’endpoint par lots

Envoyez un POST vers https://api.kvkbase.nl/v1/be/lookup/batch avec un corps JSON contenant la liste cbeNumbers et, en option, lang (nl, fr, de ou en). L’authentification passe par l’en-tête Authorization: Bearer YOUR_API_KEY.

curl -s -X POST "https://api.kvkbase.nl/v1/be/lookup/batch" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"cbeNumbers": ["0123.456.789", "BE0987654321"], "lang": "fr"}'

La réponse a cette forme :

{
  "results": {
    "0123456789": { "success": true, "data": { "name": "...", "isActive": true } },
    "0987654321": { "success": false, "error": { "error": { "code": "NOT_FOUND", "message": "..." } } }
  },
  "count": 2,
  "tokensUsed": 2
}

Les clés de results sont les numéros normalisés à dix chiffres. Remarquez le double error d’un élément en échec : le code se lit dans error.error.code. Les numéros 0123456789 et 0987654321 sont de simples exemples.

Coût et plafonds

Un lot coûte 1 token par numéro : 25 numéros, 25 tokens. Le plafond de 25 est strict, au-delà vous recevez une erreur 400. L’API vérifie aussi à l’avance que votre solde de tokens couvre tout le lot ; sinon elle répond 429 avec QUOTA_EXCEEDED et ne traite rien. Le plan gratuit offre 50 tokens par mois, donc un gros fichier suppose un plan payant, détaillé sur la page des tarifs.

Un détail qui coûte cher aux distraits : count compte tous les numéros envoyés, doublons compris. L’API n’interroge qu’une fois un numéro présent deux fois, mais vous payez les deux emplacements. Dédoublonnez donc avant l’envoi.

Étape 1 : normaliser les numéros

Un CSV issu d’un logiciel comptable mélange toutes les écritures : BE 0123.456.789, 123456789 sans zéro initial, espaces parasites, apostrophe ajoutée par Excel. Ramenez tout à dix chiffres.

  • Retirez un éventuel préfixe BE et tout caractère qui n’est pas un chiffre.
  • Si le résultat compte neuf chiffres, ajoutez un 0 en tête : Excel supprime souvent ce zéro.
  • Un numéro valide compte dix chiffres et commence par 0 ou 1.
  • Dédoublonnez, en gardant le lien avec vos lignes d’origine.

Étape 2 : filtrer par la clé de contrôle

Les deux derniers chiffres d’un numéro d’entreprise sont une clé de contrôle : 97 moins les huit premiers chiffres modulo 97. Ce calcul se fait en local, gratuitement, et il rapporte à double titre. D’abord, un seul numéro mal formé fait rejeter le lot entier : mieux vaut l’écarter avant l’envoi. Ensuite, les fautes de frappe ne consomment aucun token. Le principe est détaillé dans le guide sur la structure du numéro de TVA belge et sa clé de contrôle.

Les numéros qui échouent à ce filtre vont dans votre rapport sous la mention « format invalide ». Il ne reste qu’à les faire corriger par la personne qui entretient la base.

Étape 3 : le script

Ce script Python lit un CSV doté d’une colonne numero, normalise, filtre, envoie par lots de 25 et écrit un fichier de résultats. Il s’appuie sur requests.

import csv
import os
import re
import time
import requests

URL = "https://api.kvkbase.nl/v1/be/lookup/batch"
HEADERS = {"Authorization": f"Bearer {os.environ['KVKBASE_API_KEY']}"}

def normaliser(brut):
    chiffres = re.sub(r"\D", "", re.sub(r"^\s*BE", "", brut or "", flags=re.I))
    if len(chiffres) == 9:
        chiffres = "0" + chiffres
    return chiffres

def cle_ok(d):
    if not re.fullmatch(r"[01]\d{9}", d):
        return False
    return 97 - (int(d[:8]) % 97) == int(d[8:])

def envoyer_lot(numeros, essais=4):
    for i in range(essais):
        r = requests.post(URL, headers=HEADERS, timeout=60,
                          json={"cbeNumbers": numeros, "lang": "fr"})
        if r.status_code in (429, 502, 503):
            corps = r.json().get("error", {}) if r.content else {}
            if corps.get("code") == "QUOTA_EXCEEDED":
                raise SystemExit("Tokens mensuels épuisés, arrêt.")
            time.sleep(int(r.headers.get("Retry-After", 2 ** i)))
            continue
        r.raise_for_status()
        return r.json()["results"]
    raise RuntimeError("Lot en échec après plusieurs essais")

with open("clients.csv", newline="", encoding="utf-8") as f:
    lignes = list(csv.DictReader(f))

par_numero = {}
for ligne in lignes:
    n = normaliser(ligne["numero"])
    par_numero.setdefault(n, []).append(ligne)

valides = [n for n in par_numero if cle_ok(n)]
rapport = {n: "format invalide" for n in par_numero if n not in valides}

for i in range(0, len(valides), 25):
    lot = valides[i:i + 25]
    for numero, item in envoyer_lot(lot).items():
        if item["success"]:
            d = item["data"]
            etat = "active" if d["isActive"] else "non active"
            rapport[numero] = f"{etat} : {d['name']}"
        elif item["error"]["error"]["code"] == "NOT_FOUND":
            rapport[numero] = "introuvable"
        else:
            rapport[numero] = "erreur, à relancer"

with open("resultat.csv", "w", newline="", encoding="utf-8") as f:
    w = csv.writer(f)
    w.writerow(["numero", "resultat"])
    w.writerows(sorted(rapport.items()))

Étape 4 : introuvables et inactives

Un élément success: false avec NOT_FOUND signifie que le numéro n’apparaît pas dans les données. Il s’agit le plus souvent d’une faute de frappe qui passe par hasard la clé de contrôle, ou d’un numéro qui n’a jamais existé. Un élément success: true avec isActive: false correspond à une entreprise connue mais plus active ; consultez alors juridicalSituation pour en comprendre la raison.

Traitez ces groupes différemment :

  • Introuvable : demandez confirmation du numéro au client ou au fournisseur.
  • Non active : cherchez un éventuel successeur avant d’émettre de nouvelles factures.
  • Erreur ou indisponibilité temporaire : relancez au tour suivant, sans conclure que le numéro est inconnu.

Si tout le lot est indisponible, l’API répond par une erreur 5xx sans facturation. Au niveau d’un élément, une panne passagère porte un autre code que NOT_FOUND : considérez donc tout code différent de NOT_FOUND comme « à réessayer ».

Mettre en cache entre deux exécutions

Si vous contrôlez la même liste chaque mois, inutile de tout redemander. Conservez les résultats dans une table ou un fichier, avec le numéro pour clé et la date du contrôle. À l’exécution suivante, ne réinterrogez que les entrées plus anciennes que le délai choisi, trente jours par exemple, ainsi que celles qui étaient inactives ou indisponibles la dernière fois. La consommation de tokens baisse nettement. La réponse contient en outre source.cachedAt et source.stale : vous savez ainsi quel âge a la copie côté KVKBase.

Lectures complémentaires

Les bases de l’intégration et les codes d’erreur figurent dans le guide d’intégration de l’API, et les appels unitaires en curl, JavaScript et Python dans les exemples de code. Pour savoir quoi faire d’une entreprise inactive ou douteuse, lisez vérifier une entreprise belge avant de contracter et le guide KYC client et fournisseur. Pour tester un numéro isolé, utilisez l’outil pour rechercher un numéro d’entreprise.

Questions fréquentes

Combien de numéros puis-je envoyer par appel par lots ?
25 numéros d'entreprise au maximum par appel. Pour une liste plus longue, découpez-la vous-même en lots de 25. Chaque numéro du lot coûte 1 token.
Que se passe-t-il si un seul numéro du lot est invalide ?
L'API refuse alors le lot entier avec INVALID_CBE_NUMBER (400) et la liste des numéros fautifs. Rien n'est recherché ni facturé. Filtrez donc d'abord le format et la clé de contrôle.
Comment reconnaître un numéro qui n'existe pas ?
Chaque numéro reçoit un élément avec success. Pour un numéro inconnu, success vaut false et error contient le code NOT_FOUND. Les autres numéros du même lot restent exploitables.
Comment repérer une entreprise qui a cessé son activité ?
Regardez isActive et juridicalSituation dans la fiche. Si isActive vaut false, ou si la situation juridique n'est pas la situation normale, mieux vaut examiner le dossier avant de conserver l'entreprise comme client ou fournisseur.