Belgische bedrijven in bulk controleren met de batch-API
Een CSV met ondernemingsnummers controleren via de batch-API: normaliseren, controlegetal vooraf, niet gevonden en gestopte bedrijven, script en caching.
Op een dag heb je een export met duizend klanten of leveranciers en de vraag welke ondernemingsnummers nog kloppen. Eén voor één opzoeken kan, maar de batch-endpoint van KVKBase doet dit efficiënter: tot 25 nummers per oproep, met per nummer een eigen resultaat. Deze gids loopt door het verwerken van een CSV, van het opschonen van de nummers tot een Python-script en caching. Het gaat om officiële Belgische bedrijfsgegevens, opgehaald via de API.
Hoe de batch-endpoint werkt
Je stuurt een POST naar https://api.kvkbase.nl/v1/be/lookup/batch met een JSON-body die een lijst cbeNumbers bevat en optioneel lang (nl, fr, de of en). Authenticatie loopt via de header 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": "nl"}'
Het antwoord heeft deze vorm:
{
"results": {
"0123456789": { "success": true, "data": { "name": "...", "isActive": true } },
"0987654321": { "success": false, "error": { "error": { "code": "NOT_FOUND", "message": "..." } } }
},
"count": 2,
"tokensUsed": 2
}
De sleutels in results zijn de genormaliseerde nummers van tien cijfers. Let op de dubbele error bij een mislukt item: de foutcode staat in error.error.code. De waarden 0123456789 en 0987654321 zijn hier voorbeelden.
Tokens en limieten
Een batch kost 1 token per nummer, dus een batch van 25 kost 25 tokens. Het limiet van 25 per oproep is hard: meer nummers geeft een 400. Daarnaast controleert de API vooraf of je nog genoeg tokens over hebt voor de hele batch. Zo niet, dan krijg je een 429 met QUOTA_EXCEEDED en wordt er niets verwerkt. Het gratis plan heeft 50 tokens per maand, voor een grote CSV heb je dus een betaald plan nodig, zie de prijzen.
Let ook op count in het antwoord: dat telt alle nummers die je meestuurde, ook dubbele. De API zoekt een nummer dat twee keer voorkomt maar één keer op, toch betaal je voor beide plaatsen in de lijst. Dedupliceer dus zelf voor je verstuurt.
Stap 1: nummers normaliseren
Een CSV uit een boekhoudpakket bevat allerlei schrijfwijzen: BE 0123.456.789, 123456789 zonder voorloopnul, nummers met spaties of een apostrof van Excel. Normaliseer eerst naar tien cijfers.
- Haal een eventuele
BE-prefix weg en verwijder alles wat geen cijfer is. - Heeft het resultaat negen cijfers, voeg dan een
0vooraan toe. Excel verwijdert die nul vaak. - Een nummer van tien cijfers begint met 0 of 1.
- Dedupliceer de lijst en bewaar de koppeling naar je oorspronkelijke rijen.
Stap 2: het controlegetal als voorfilter
De laatste twee cijfers van een ondernemingsnummer zijn een controlegetal: 97 min de eerste acht cijfers modulo 97. Dat kun je lokaal en gratis controleren. Het loont om twee redenen. Een batch met één foutief geformatteerd nummer wordt in zijn geheel geweigerd, dus je wilt dat nummer eruit hebben voor je verstuurt. Daarnaast hoef je voor typfouten geen enkel token te besteden. Meer uitleg over dit mechanisme vind je in de gids over de opbouw van het btw-nummer en het controlegetal.
Nummers die dit filter niet halen, schrijf je in je rapport weg als “ongeldig formaat”. Je hoeft er niets meer mee te doen, behalve ze laten corrigeren door wie de gegevens beheert.
Stap 3: het script
Het volgende Python-script leest een CSV met een kolom ondernemingsnummer, normaliseert, filtert, verstuurt per 25 en schrijft een resultaat-CSV. Het gebruikt 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 normalize(raw):
digits = re.sub(r"\D", "", re.sub(r"^\s*BE", "", raw or "", flags=re.I))
if len(digits) == 9:
digits = "0" + digits
return digits
def checksum_ok(d):
if not re.fullmatch(r"[01]\d{9}", d):
return False
return 97 - (int(d[:8]) % 97) == int(d[8:])
def post_batch(numbers, attempts=4):
for i in range(attempts):
r = requests.post(URL, headers=HEADERS, timeout=60,
json={"cbeNumbers": numbers, "lang": "nl"})
if r.status_code in (429, 502, 503):
body = r.json().get("error", {}) if r.content else {}
if body.get("code") == "QUOTA_EXCEEDED":
raise SystemExit("Maandelijkse tokens op, stop.")
time.sleep(int(r.headers.get("Retry-After", 2 ** i)))
continue
r.raise_for_status()
return r.json()["results"]
raise RuntimeError("Batch mislukt na meerdere pogingen")
with open("klanten.csv", newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
by_number = {}
for row in rows:
n = normalize(row["ondernemingsnummer"])
by_number.setdefault(n, []).append(row)
valid = [n for n in by_number if checksum_ok(n)]
report = {n: "ongeldig formaat" for n in by_number if n not in valid}
for i in range(0, len(valid), 25):
chunk = valid[i:i + 25]
for number, item in post_batch(chunk).items():
if item["success"]:
d = item["data"]
status = "actief" if d["isActive"] else "niet actief"
report[number] = f"{status}: {d['name']}"
elif item["error"]["error"]["code"] == "NOT_FOUND":
report[number] = "niet gevonden"
else:
report[number] = "fout, later opnieuw proberen"
with open("resultaat.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["ondernemingsnummer", "resultaat"])
w.writerows(sorted(report.items()))
Stap 4: niet gevonden en niet actief
Een resultaat van success: false met NOT_FOUND betekent dat het nummer niet in de gegevens staat. Meestal is het een typfout die toevallig het controlegetal haalt, of een nummer dat nooit bestond. Een nummer met success: true en isActive: false is bekend maar niet meer actief. Lees dan ook juridicalSituation om te zien waarom.
Behandel deze groepen verschillend:
- Niet gevonden: laat het nummer navragen bij de klant of leverancier.
- Niet actief: zoek uit of er een opvolger is voor je nieuwe facturen verstuurt.
- Fout of tijdelijk onbeschikbaar: opnieuw proberen in een volgende ronde, niet als “onbekend” bestempelen.
Is het resultaat van een hele batch onbeschikbaar, dan geeft de API een 5xx zonder kosten. Op itemniveau kan een tijdelijke fout een andere code hebben dan NOT_FOUND. Behandel alles wat geen NOT_FOUND is dus als “later opnieuw”.
Caching tussen runs
Wie elke maand dezelfde lijst controleert, hoeft niet elk bedrijf opnieuw op te vragen. Bewaar de resultaten in een eigen tabel of bestand met het nummer als sleutel en het tijdstip van de controle. Haal bij de volgende run alleen op wat ouder is dan de gekozen termijn, bijvoorbeeld dertig dagen, plus alles wat de vorige keer niet actief of onbeschikbaar was. Zo verbruik je veel minder tokens. Het antwoord bevat bovendien source.cachedAt en source.stale, zodat je weet hoe oud een kopie aan onze kant is.
Verder lezen
De basis van de integratie en de foutcodes staan in de gids over het integreren van de API, met losse oproepen in curl, JavaScript en Python in de gids met codevoorbeelden. Wil je weten wat je daarna met een gestopt of verdacht bedrijf doet, lees dan een bedrijf controleren voor je zaken doet en de KYC-gids voor klanten en leveranciers. Een enkel nummer kan je meteen proberen met de tool om een ondernemingsnummer op te zoeken.