KVKBase Team

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.

belgieapibatchdata-opschoning

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 0 vooraan 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.

Veelgestelde vragen

Hoeveel nummers kan ik in één batch-oproep sturen?
Maximaal 25 ondernemingsnummers per oproep. Een grotere lijst knip je zelf in stukken van 25. Elk nummer in de batch kost 1 token.
Wat gebeurt er als één nummer in de batch ongeldig is?
Dan weigert de API de hele batch met INVALID_CBE_NUMBER (400) en een lijst van de foute nummers. Er wordt niets opgezocht en niets aangerekend. Filter dus vooraf op formaat en controlegetal.
Hoe herken ik een nummer dat niet bestaat?
In het antwoord krijgt elk nummer een item met success. Bij een onbekend nummer is success false en bevat error de code NOT_FOUND. De andere nummers in dezelfde batch blijven gewoon bruikbaar.
Hoe herken ik een gestopt bedrijf?
Kijk naar isActive en juridicalSituation in de gegevens van het bedrijf. Is isActive false of verschilt de juridische situatie van de normale situatie, dan verdient het bedrijf een nader onderzoek voor je het als klant of leverancier behoudt.