CompanyBelgium

KBO/BCE-API in Node.js en TypeScript: moderne integratiegids met voorbeelden

De KBO/BCE-API integreren in een moderne Node.js / TypeScript-stack. Volledige tutorial: getypeerde client met native fetch, retry, LRU-cache, Next.js- en Express-integratie, webhook-afhandeling. Alle code compileert in strict mode.

7 mei 20268 min leestijd

In het kort

De KBO/BCE-API integreren in Node.js en TypeScript lukt in minder dan 200 regels: een getypeerde client met native fetch, exponentiële retry, LRU-cache en HMAC-ondertekende webhooks. Deze tutorial behandelt Express en Next.js App Router, alle code compileert in strict mode onder Node.js 22+.

Het doel

Aan het einde van deze tutorial heeft u:

  • Een getypeerde TypeScript-client voor de KBO/BCE-API
  • Een Express- of Next.js Route Handler-wrapper
  • Een strategie retry + LRU-cache
  • Webhook-afhandeling om op wijzigingen te reageren
  • Een schone foutlaag

De code gebruikt Node.js 22+ en de native fetch — geen extra HTTP-dependency.

1. Setup

Terminal
1
2
3
4
mkdir bce-node-tuto && cd bce-node-tuto
pnpm init
pnpm add typescript @types/node tsx
pnpm tsc --init

Minimaal tsconfig.json:

JSON
1
2
3
4
5
6
7
8
9
10
11
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "dist"
  }
}
.env:
Terminal
1
2
3
COMPANY_BELGIUM_API_KEY=pk_live_xxxxxxxxxxxx
COMPANY_BELGIUM_API_SECRET=sk_live_xxxxxxxxxxxx
COMPANY_BELGIUM_BASE_URL=https://api.companybelgium.be/v1

2. De types

Maak types.ts:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
export interface Address {
  street: string
  number: string
  postalCode: string
  municipality: string
  country: string
  fullAddress: string
}

export interface NaceCode {
  code: string
  label: string
  version: class="code-string">"2003" | class="code-string">"2008" | class="code-string">"2025"
}

export interface Company {
  enterpriseNumber: string
  vatNumber: string | null
  name: string
  legalForm: string
  status: class="code-string">"AC" | class="code-string">"ST"
  startDate: string
  endDate: string | null
  address: Address
  mainActivity: NaceCode | null
  establishmentsCount: number
}

export interface SearchResult {
  enterpriseNumber: string
  name: string
  status: class="code-string">"AC" | class="code-string">"ST"
  municipality: string
}

export interface ApiResponse<T> {
  success: boolean
  data: T
  error?: string
  timestamp: string
}

3. De getypeerde client

Maak bce-client.ts:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import type { Company, SearchResult, ApiResponse } from class="code-string">"./types"

export interface BCEClientOptions {
  baseUrl?: string
  apiKey?: string
  apiSecret?: string
  timeoutMs?: number
}

export class BCEClient {
  private baseUrl: string
  private headers: Record<string, string>
  private timeoutMs: number

  constructor(opts: BCEClientOptions = {}) {
    this.baseUrl = opts.baseUrl ?? process.env.COMPANY_BELGIUM_BASE_URL!
    const key = opts.apiKey ?? process.env.COMPANY_BELGIUM_API_KEY!
    const secret = opts.apiSecret ?? process.env.COMPANY_BELGIUM_API_SECRET!
    this.headers = {
      class="code-string">"X-API-Key": key,
      class="code-string">"X-API-Secret": secret,
      class="code-string">"Accept": class="code-string">"application/json",
    }
    this.timeoutMs = opts.timeoutMs ?? 10_000
  }

  private async request<T>(path: string, init?: RequestInit): Promise<T> {
    const controller = new AbortController()
    const timer = setTimeout(() => controller.abort(), this.timeoutMs)
    try {
      const res = await fetch(class="code-string">`${this.baseUrl}${path}`, {
        ...init,
        headers: { ...this.headers, ...(init?.headers ?? {}) },
        signal: controller.signal,
      })
      if (!res.ok) {
        const body = await res.text()
        throw new BCEError(res.status, body, res.headers)
      }
      const json = (await res.json()) as ApiResponse<T>
      return json.data
    } finally {
      clearTimeout(timer)
    }
  }

  async getCompany(enterpriseNumber: string): Promise<Company> {
    const clean = enterpriseNumber.replace(/[.\s]/g, class="code-string">"")
    return this.request<Company>(class="code-string">`/companies/${clean}`)
  }

  async search(query: string, limit = 20): Promise<SearchResult[]> {
    const params = new URLSearchParams({ q: query, limit: String(limit) })
    return this.request<SearchResult[]>(class="code-string">`/companies/search?${params}`)
  }
}

export class BCEError extends Error {
  constructor(
    public status: number,
    public body: string,
    public headers: Headers,
  ) {
    super(class="code-string">`BCE API error ${status}: ${body}`)
    this.name = class="code-string">"BCEError"
  }

  get retryAfterMs(): number | null {
    const v = this.headers.get(class="code-string">"Retry-After")
    if (!v) return null
    const n = Number(v)
    return Number.isFinite(n) ? n * 1000 : null
  }
}

4. Exponentiële retry

retry.ts:
TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { BCEError } from class="code-string">"./bce-client"

const TRANSIENT_STATUSES = new Set([429, 500, 502, 503, 504])

export async function withRetry<T>(
  fn: () => Promise<T>,
  opts: { maxAttempts?: number; baseDelayMs?: number } = {},
): Promise<T> {
  const max = opts.maxAttempts ?? 4
  const base = opts.baseDelayMs ?? 500

  let lastError: unknown
  for (let attempt = 0; attempt < max; attempt++) {
    try {
      return await fn()
    } catch (e) {
      lastError = e
      const shouldRetry =
        (e instanceof BCEError && TRANSIENT_STATUSES.has(e.status)) ||
        (e instanceof TypeError && e.message.includes(class="code-string">"fetch")) ||
        (e instanceof Error && e.name === class="code-string">"AbortError")
      if (!shouldRetry || attempt === max - 1) throw e
      const explicit = e instanceof BCEError ? e.retryAfterMs : null
      const delay = explicit ?? base * 2 ** attempt
      await new Promise((r) => setTimeout(r, delay))
    }
  }
  throw lastError
}

5. LRU-cache in geheugen

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export class LRUCache<K, V> {
  private store = new Map<K, { value: V; expires: number }>()
  constructor(
    private maxSize: number,
    private ttlMs: number,
  ) {}

  get(key: K): V | undefined {
    const item = this.store.get(key)
    if (!item) return undefined
    if (item.expires < Date.now()) {
      this.store.delete(key)
      return undefined
    }
    this.store.delete(key)
    this.store.set(key, item)
    return item.value
  }

  set(key: K, value: V): void {
    if (this.store.size >= this.maxSize) {
      const oldest = this.store.keys().next().value
      if (oldest !== undefined) this.store.delete(oldest)
    }
    this.store.set(key, { value, expires: Date.now() + this.ttlMs })
  }
}

6. Express-wrapper

server.ts:
TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import express from class="code-string">"express"
import { BCEClient, BCEError } from class="code-string">"./bce-client"
import { withRetry } from class="code-string">"./retry"

const app = express()
const bce = new BCEClient()

app.get(class="code-string">"/companies/:number", async (req, res) => {
  try {
    const company = await withRetry(() => bce.getCompany(req.params.number))
    res.json(company)
  } catch (e) {
    if (e instanceof BCEError && e.status === 404) {
      res.status(404).json({ error: class="code-string">"Company not found" })
      return
    }
    res.status(502).json({ error: class="code-string">"Upstream error" })
  }
})

app.listen(3000, () => console.log(class="code-string">"Listening on :3000"))

7. Next.js-wrapper (App Router)

app/api/companies/[number]/route.ts:
TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { NextResponse } from class="code-string">"next/server"
import { BCEClient, BCEError } from class="code-string">"@/lib/bce-client"
import { withRetry } from class="code-string">"@/lib/retry"

const bce = new BCEClient()

export async function GET(
  _req: Request,
  { params }: { params: Promise<{ number: string }> },
) {
  const { number } = await params
  try {
    const company = await withRetry(() => bce.getCompany(number))
    return NextResponse.json(company)
  } catch (e) {
    if (e instanceof BCEError && e.status === 404) {
      return NextResponse.json({ error: class="code-string">"Not found" }, { status: 404 })
    }
    return NextResponse.json({ error: class="code-string">"Upstream error" }, { status: 502 })
  }
}

8. Webhook-ontvangst

Company Belgium stuurt een POST naar uw endpoint bij wijzigingen. Verifieer de handtekening:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { createHmac } from class="code-string">"node:crypto"

const WEBHOOK_SECRET = process.env.COMPANY_BELGIUM_WEBHOOK_SECRET!

function verifySignature(rawBody: string, signature: string): boolean {
  const expected = createHmac(class="code-string">"sha256", WEBHOOK_SECRET)
    .update(rawBody)
    .digest(class="code-string">"hex")
  return signature === expected
}

app.post(
  class="code-string">"/webhooks/bce",
  express.raw({ type: class="code-string">"application/json" }),
  (req, res) => {
    const signature = req.headers[class="code-string">"x-signature"] as string
    const rawBody = req.body.toString(class="code-string">"utf-8")
    if (!verifySignature(rawBody, signature)) {
      return res.status(401).end()
    }
    const event = JSON.parse(rawBody) as {
      type: string
      enterpriseNumber: string
      data: unknown
    }
    console.log(class="code-string">"Event ontvangen:", event.type, event.enterpriseNumber)
    res.status(200).end()
  },
)

Typische events: company.updated, company.director.added, company.ubo.changed, company.accounts.filed.

9. Te vermijden fouten

  • Een client per verzoek aanmaken — verliest keep-alive. Module-level instantie.
  • AbortError negeren — zonder timeout blokkeren workers.
  • Webhook synchroon verwerken — afzender krijgt timeout. Zet in queue, antwoord direct 200.
  • Handtekening niet verifiëren — iedereen kan uw endpoint spammen.
  • Hardcoded keys — altijd via env. Zie API-beveiligingsprincipes.
  • Hoe Company Belgium uw Node.js-development versnelt

    Op pro-plannen biedt Company Belgium:

    • Officiële TypeScript-SDK (@companybelgium/sdk) met volledige types en geïntegreerde retry/cache
    • OpenAPI consumeerbaar door openapi-typescript om types up-to-date te genereren
    • Webhooks met HMAC SHA-256-handtekening
    • Rate-limit headers: X-RateLimit-Remaining, X-RateLimit-Reset

    Verder lezen: zie de BCE-API-vergelijking, de endpoints-referentie, de Python-tutorial en de rate-limit / caching-architectuur.

    Samenvatting

    De Node.js / TypeScript-integratie van de KBO/BCE-API past in 200 getypeerde regels:

    • Native fetch + strikte types
    • Exponentiële retry
    • LRU-cache
    • Adapter Express of Next.js
    • Ondertekende webhooks

    Hergebruik van de client, timeout-afhandeling, geverifieerde HMAC-handtekening — u hebt een production-grade integratie.

    Veelgestelde vragen

    Hoe authenticeer ik mijn verzoeken aan de KBO/BCE-API in Node.js?

    U stuurt twee HTTP-headers mee: X-API-Key met uw publieke sleutel (pk_live_...) en X-API-Secret met uw geheime sleutel (sk_live_...). Deze sleutels vindt u in uw Company Belgium-dashboard. Stel ze nooit bloot aan de clientzijde: bewaar ze in omgevingsvariabelen aan de serverzijde.

    Wat is de beste cachestrategie voor de KBO/BCE-API in TypeScript?

    Een in-memory LRU-cache met een TTL van 5 minuten volstaat voor de meeste use cases. KBO-gegevens worden zelden in real time bijgewerkt. Voor een gedistribueerde architectuur kunt u Redis gebruiken als gedeelde cache tussen meerdere Node.js-instanties. Het is belangrijk om niet bij elk gebruikersverzoek de API aan te roepen.

    Hoe ga ik om met 429-fouten (rate limit) van de KBO/BCE-API in Node.js?

    Implementeer exponentiële retry: wacht 500 ms bij de eerste poging, 1 000 ms bij de tweede, 2 000 ms bij de derde. De API stuurt een Retry-After-header wanneer u het quotum overschrijdt; gebruik die waarde bij voorkeur. Raadpleeg ook de pagina over rate limits en caching om uw architectuur correct te dimensioneren.

    Hoe ontvang ik KBO/BCE-webhooks en verifieer ik hun handtekening in Node.js?

    Company Belgium ondertekent elke webhook met HMAC SHA-256 en stuurt de handtekening in de X-Signature-header. Gebruik aan de Node.js-kant de native crypto-module met createHmac om de handtekening te herberekenen vanuit het ruwe verzoeklichaam en te vergelijken met de ontvangen waarde. Antwoord altijd onmiddellijk met 200 en verwerk de event asynchroon.

    Klaar om te beginnen?

    Maak gratis een account aan en ontvang uw API-sleutels in enkele minuten.

    Reacties

    Reacties laden…

    Laat een reactie achter

    Niet gepubliceerd — alleen om u te verwittigen.

    Reacties worden gemodereerd voor publicatie.

    Gerelateerde artikelen