CompanyBelgium

API KBO/BCE en Node.js et TypeScript : guide d'intégration moderne avec exemples

Intégrer l'API BCE/KBO dans une stack Node.js / TypeScript moderne. Tutoriel complet : client typé avec fetch natif, retry, cache LRU, intégration Next.js et Express, gestion des webhooks. Tout le code compile en strict mode.

7 mai 20268 min de lecture

En bref

Intégrer l'API BCE/KBO en Node.js et TypeScript se fait en moins de 200 lignes : un client typé avec fetch natif, une stratégie de retry exponentiel, un cache LRU et des webhooks signés HMAC. Ce tutoriel couvre Express et Next.js App Router, tout compile en strict mode sous Node.js 22+.

L'objectif

À la fin de ce tutoriel, vous aurez :

  • Un client TypeScript typé pour l'API BCE/KBO
  • Un wrapper Express ou Next.js Route Handler
  • Une stratégie retry + cache LRU
  • La gestion des webhooks pour réagir aux changements
  • Une couche d'erreurs propre

Le code utilise Node.js 22+ et le fetch natif — pas de dépendance HTTP supplémentaire.

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
tsconfig.json minimal :
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. Les types

Créez 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" class="code-comment">// AC = Actif, ST = Cessé
  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. Le client typé

Créez 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. Retry exponentiel

Créez 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
}

Wrapper l'usage :

TypeScript
1
const company = await withRetry(() => client.getCompany(class="code-string">"0200.065.765"))

5. Cache LRU en mémoire

Pour Node 20+, Map suffit pour un LRU minimal :

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
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
    }
    class="code-comment">// refresh LRU position
    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 })
  }
}

Intégrer au client :

TypeScript
1
2
3
4
5
6
7
8
9
10
private cache = new LRUCache<string, Company>(500, 5 * 60_000)

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

6. Wrapper Express

server.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
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.get(class="code-string">"/search", async (req, res) => {
  const q = String(req.query.q ?? class="code-string">"")
  if (!q) return res.status(400).json({ error: class="code-string">"Query required" })
  try {
    const results = await withRetry(() => bce.search(q, 20))
    res.json(results)
  } catch {
    res.status(502).json({ error: class="code-string">"Upstream error" })
  }
})

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

7. Wrapper Next.js (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 })
  }
}

Le client est réutilisé entre requêtes (instance module-level) — keep-alive HTTP est conservé.

8. Réception de webhooks

L'API Company Belgium envoie un POST sur votre endpoint quand une entreprise change. Vérifiez la signature et traitez :

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
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
    }
    class="code-comment">// mettez l'event en queue, traitez en async
    console.log(class="code-string">"Event reçu:", event.type, event.enterpriseNumber)
    res.status(200).end()
  },
)

Événements typiques : company.updated, company.director.added, company.ubo.changed, company.accounts.filed.

9. Erreurs à éviter

  • Créer un client par requête — perd le keep-alive. Instance module-level.
  • Ignorer AbortError — sans timeout, vos workers se bloquent.
  • Traiter le webhook en synchrone — vous timeout sur l'expéditeur. Mettez en queue puis répondez 200 immédiatement.
  • Ne pas vérifier la signature — n'importe qui peut spammer votre endpoint.
  • Hardcode des clés — toujours via env. Voir bonnes pratiques de sécurité API.
  • Comment Company Belgium accélère votre dev Node.js

    Sur les plans pro, Company Belgium fournit :

    • SDK officiel TypeScript (@companybelgium/sdk) avec types complets et retry/cache intégrés
    • OpenAPI consommable par openapi-typescript pour générer vos types à jour
    • Webhooks avec signature HMAC SHA-256
    • Headers de rate limit : X-RateLimit-Remaining, X-RateLimit-Reset

    Pour aller plus loin : voir le comparatif des API BCE, la référence des endpoints, le tuto Python et l'architecture rate limit / caching.

    En résumé

    L'intégration Node.js / TypeScript de l'API BCE/KBO tient en 200 lignes typées :

    • fetch natif + types stricts
    • Retry exponentiel
    • Cache LRU
    • Adapter Express ou Next.js
    • Webhooks signés

    Reuse du client, gestion du timeout, signature HMAC vérifiée — vous avez une intégration de niveau production.

    Questions fréquentes

    Comment authentifier mes requêtes vers l'API BCE/KBO en Node.js ?

    Vous devez envoyer deux en-têtes HTTP : X-API-Key avec votre clé publique (pk_live_...) et X-API-Secret avec votre clé secrète (sk_live_...). Ces clés sont disponibles dans votre tableau de bord Company Belgium. Ne les exposez jamais côté client : stockez-les dans des variables d'environnement côté serveur.

    Quelle est la meilleure stratégie de cache pour l'API BCE/KBO en TypeScript ?

    Un cache LRU en mémoire avec un TTL de 5 minutes est suffisant pour la plupart des cas d'usage. Les données BCE évoluent rarement en temps réel. Pour une architecture distribuée, vous pouvez utiliser Redis comme cache partagé entre plusieurs instances Node.js. L'important est d'éviter d'appeler l'API à chaque requête utilisateur.

    Comment gérer les erreurs 429 (rate limit) de l'API BCE/KBO en Node.js ?

    Implémentez un retry exponentiel : attendez 500 ms au premier retry, 1 000 ms au second, 2 000 ms au troisième. L'API renvoie un en-tête Retry-After quand vous dépassez le quota ; utilisez cette valeur en priorité. Consultez aussi la page sur les rate limits et le caching pour dimensionner correctement votre architecture.

    Comment recevoir les webhooks BCE/KBO et vérifier leur signature en Node.js ?

    Company Belgium signe chaque webhook avec HMAC SHA-256 et envoie la signature dans l'en-tête X-Signature. Côté Node.js, utilisez le module crypto natif avec createHmac pour recalculer la signature à partir du corps brut de la requête et la comparer avec la valeur reçue. Répondez toujours 200 immédiatement, puis traitez l'event de façon asynchrone.

    Prêt à commencer ?

    Créez votre compte gratuitement et obtenez vos clés API en quelques minutes.

    Commentaires

    Chargement des commentaires…

    Laisser un commentaire

    Non publié — sert uniquement à vous notifier.

    Les commentaires sont modérés avant publication.

    Articles similaires