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.
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
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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
AbortError — sans timeout, vos workers se bloquent.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-typescriptpour 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 :
fetchnatif + 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.
Commentaires
Articles similaires

Expert-comptable : automatiser l'analyse financière de votre portefeuille clients
Un expert-comptable gère 50 à 300 dossiers clients. Analyser manuellement les comptes annuels, calculer les ratios, repérer les signaux d'alerte et préparer un rapport pour chaque réunion annuelle, c'est plusieurs centaines d'heures par an. Voici comment automatiser 80% de ce travail tout en gagnant en qualité de service.

Calculer la limite de crédit d'un client B2B : 4 méthodes éprouvées
Combien d'encours pouvez-vous accorder en toute sécurité à un client B2B ? Découvrez les quatre méthodes utilisées par les assureurs-crédit et les services financiers des PME belges : plafond fonds propres, plafond chiffre d'affaires, plafond actifs et scoring financier — avec exemples chiffrés.

Recherche d'entreprises belges en langage naturel : le guide complet de /recherche
Tapez « comptables à Liège », « restaurants 1000 Bruxelles » ou un numéro BCE : la barre /recherche transforme votre phrase en filtres NACE, code postal, dénomination, puis renvoie une page enrichie de statistiques sectorielles. Tour d'horizon des cas d'usage, des raccourcis URL et des subtilités linguistiques.
