Aller au contenu
REST · MCP · read-only

Developers

Belgian company data your software can read: identity and status from the KBO/BCE register, the verified act timeline extracted from the Belgisch Staatsblad / Moniteur belge, officers with currency semantics, and parsed NBB annual accounts. Two surfaces over the same corpus, one API key for both: a JSON REST API for conventional clients, and a Model Context Protocol endpoint for AI agents.

This reference is English only, on purpose: every identifier, tool name and error string in the surface below is English. The product itself is Dutch, French and English.


The provability promise

Every fact we serve carries the identifier of the document it came from, so your users can check it and your agent can cite it:

  • Act-derived facts carry numac, the Belgian gazette publication number, plus the publication date.
  • Mandates carry the numac and publication date of the act that established them.
  • Register facts (name, legal form, seat, status, NACE) come from the KBO/BCE open data.
  • Financial figures are the lines as filed with the National Bank; ratios are derived from them and labelled as derived.

The other half of the promise is admitting what is missing. Deep act extraction does not cover the whole corpus yet, so the MCP surface exposes get_extraction_status: it tells you how many gazette acts are on file for a company and how many have been extracted, so an empty timeline is explainable rather than mysterious. Sector-inapplicable financial ratios come back as an explicit not-applicable marker with a reason, never as a misleading number.


Basics
REST base URLhttps://checked.be/api/v1
MCP endpointhttps://checked.be/mcp
AuthenticationX-Api-Key request header, on both surfaces
FormatJSON. REST fields are snake_case; MCP tool results are camelCase (the two serializers differ, this is not a typo)
MethodsRead-only. Every documented endpoint is a GET; MCP is JSON-RPC over POST but exposes no write tool
Rate limit60 requests per minute (see the rate-limit section, the two surfaces count differently)
CORSNot enabled. Call from your server, not from a browser
CachingEvery /api/v1 response carries Cache-Control: public, max-age=300, except a response built on a source that did not answer: that one is no-store, so a shared cache cannot hand a degraded answer to the next five minutes of callers
OpenAPINone published. This page is the specification
VersioningIn the path (/v1/). Additive changes ship without a version bump; see the changelog

Authentication

Send your key in the X-Api-Key header. There is no bearer-token form and no query-string form. Keys are issued by hand today: mail enterprise@checked.be and say what you are building. There is no self-service key page yet, and this reference will not pretend otherwise.

curl -sS -H "X-Api-Key: $CHECKED_API_KEY" \
  https://checked.be/api/v1/companies/0400378485

The gate is fail-closed: if no keys are provisioned on the server, every call gets 401, including calls that carry a key.


REST endpoints

Four endpoints. The {kbo} path segment accepts any form whose digits end in a 10-digit enterprise number: 0400378485, 0400.378.485 and BE0400378485 all resolve to the same company.

GET /api/v1/companies/{kbo}

The dossier spine: identity, seat, risk band, insolvency rollup and the mandate list. risk is never null, but an unscored company gets band Unknown and score_out_of100: null, never a zero: on this API a higher score means safer, so a zero for an unmeasured company would read as the most alarming value there is. probability and altman_z are withheld along with the band, because both come from the same model run. band is PascalCase and one of Unknown, Low, Moderate, Elevated, High, Severe.

Read coverage first. Our insolvency timeline is a live read against a separate system, and it can time out. When it does, coverage.insolvency_timeline_read is false, coverage.stages_degraded contains insolvency_timeline, and every field that stage feeds comes back null rather than a reassuring zero: rollup, insolvency_event_count and admin_event_count. Null on those three means we do not know, and it is never safe to treat it as zero. mandates is null on the same condition, but an empty list there is a weaker statement: the mandate read is a nested best-effort inside the same stage, so [] means "no mandates returned", not "this company has no mandates". A degraded dossier is never cached at the HTTP layer (Cache-Control: no-store); the dossier bundle behind it is rebuilt after at most three minutes, which is what the Retry-After on the 503 reflects. as_of is when this response was composed. address is null when we hold no address.

status is a registration state, not a solvency state. It is derived from the company still being present in the latest KBO extract, and all 33,033 companies the register records as an open bankruptcy are active by it (measured 2026-08-09). The field to threshold on is register_situation_code, the register's own legal-situation code, verbatim: 000 ordinary, 050 open bankruptcy, 012 voluntary dissolution. Null there means we did not read the register on this request, never that there is nothing to report. It is the only corpus-complete insolvency source we have: our own gazette extraction, which is what rollup counts, misses 8,683 of those 33,033.

{
  "enterprise_number": "0400378485",
  "legal_name": "COLRUYT GROUP",
  "legal_form": "610",
  "primary_nace": "47110",
  "status": "active",
  "address": { "street": "...", "zip": "1500", "city": "Halle" },
  "risk": {
    "band": "Low", "score_out_of100": 88, "probability": 0.0121,
    "altman_z": 3.42, "model_version": "...", "evaluated_at": "2026-07-30T02:11:04+00:00"
  },
  "rollup": null,
  "insolvency_event_count": 0,
  "admin_event_count": 4,
  "mandates": [
    { "role": "...", "role_kind": "...", "name": "...", "address": null,
      "from": "2019-05-14", "to": null, "source": "...", "source_numac": "2019..." }
  ],
  "coverage": {
    "insolvency_timeline_read": true, "risk_scored": true, "stages_degraded": []
  },
  "as_of": "2026-08-09T11:04:22+00:00",
  "register_situation_code": "000"
}

The same company when the timeline read did not come back. The risk block is unaffected (it is a local read on a different stage), and the four timeline-fed fields are null, not 0 and not []:

{
  "enterprise_number": "0400378485",
  "legal_name": "COLRUYT GROUP",
  "risk": {
    "band": "Low", "score_out_of100": 88, "probability": 0.0121,
    "altman_z": 3.42, "model_version": "...", "evaluated_at": "2026-07-30T02:11:04+00:00"
  },
  "rollup": null,
  "insolvency_event_count": null,
  "admin_event_count": null,
  "mandates": null,
  "coverage": {
    "insolvency_timeline_read": false, "risk_scored": true,
    "stages_degraded": ["insolvency_timeline"]
  },
  "as_of": "2026-08-09T11:04:22+00:00",
  "register_situation_code": "000"
}
GET /api/v1/companies/{kbo}/lifecycle

A bare JSON array of legal-lifecycle events, no envelope. source is one of faillissement, reorg, insolvency_interim, kbo_admin; severity is low, med or high. Not paginated: you get the whole timeline. An EMPTY array is a statement that this company has no legal-lifecycle events, so we only send it when the source answered; if it did not, you get a 503 instead. Events we do hold are always served, degraded read or not.

[
  { "source": "faillissement", "severity": "high", "numac": "2026...",
    "pub_date": "2026-05-20", "event_kind": "...", "court": "...",
    "event_date": "2026-05-14", "detail": "..." }
]
GET /api/v1/companies/{kbo}/rollup

The compact insolvency aggregate on its own, for cheap screening of a portfolio. Returns 404 when the company exists but has no rollup on file, and 503 when the source did not answer. Those two used to be the same 404, which is how an outage became a clean screening result: the 404 is now a claim we can back, so treat only the 404 as "nothing on record".

{
  "total_events": 3, "interim_signals": 1, "reorg_events": 1,
  "faillissement_events": 1, "first_signal_date": "2026-01-05",
  "last_signal_date": "2026-05-20", "has_open_faillissement": true
}
GET /api/v1/search?q={query}&limit={n}

Company lookup by name, enterprise number, VAT number or NACE code. limit defaults to 20 and is clamped to 1..50. A blank or missing q returns [] with status 200, not an error. Not paginated and no total count: this is a resolver, not a corpus dump. match_kind tells you which lane matched (name, address, kbo, nace). On this endpoint kind is always company and href/subtitle are always null; they exist so the shape matches the internal omni-search wire format.

curl -sS -H "X-Api-Key: $CHECKED_API_KEY" \
  'https://checked.be/api/v1/search?q=colruyt&limit=5'

[
  { "enterprise_number": "0400378485", "legal_name": "COLRUYT GROUP",
    "slug": "colruyt-group-0400378485", "city": "Halle", "primary_nace": "47110",
    "match_kind": "name", "matched_address": null, "kind": "company",
    "href": null, "subtitle": null }
]

Errors

Every error carries a JSON body with a single error string. Match on the status code, not on the wording.

401{"error":"Missing X-Api-Key header."}
401{"error":"Invalid API key."}
401{"error":"API access is not enabled."}
404{"error":"Unknown enterprise number."}
429{"error":"Rate limit exceeded: ..."} plus Retry-After: 60
503{"error":"Insolvency source unavailable..."} plus Retry-After: 180 and Cache-Control: no-store. Only on /lifecycle and /rollup, where an empty answer would otherwise read as "nothing on record". It means we could not look, not that there is nothing

Rate limits
REST /api/v1/*60 requests per minute, counted per client IP, fixed window
MCP /mcp60 requests per minute, counted per API key when a key is present, otherwise per IP
Over the limit429 with Retry-After: 60
Repeated bad keysMore than 20 failed /mcp authentications in a minute from one IP get 429 instead of 401

One caveat worth knowing before you build a batch job: the REST budget is per IP and is shared with the unauthenticated /internal/search and /health/status endpoints. If you need sustained throughput, higher limits are a mail away rather than something to work around.


Model Context Protocol

https://checked.be/mcp speaks MCP over streamable HTTP: JSON-RPC 2.0 on POST, an event stream on GET, session id in the Mcp-Session-Id header. Protocol versions 2024-11-05 through 2025-11-25 are negotiated at initialize. Server identity is Checked.be version 1.0.0. The legacy HTTP+SSE transport is disabled, so that one path is the entire surface: there are no separate stream or message endpoints to configure. Six tools, no resources, no prompts, nothing that writes.

The endpoint requires the same X-Api-Key header as the REST API. A client configuration without it will fail at initialize with a 401.

{
  "mcpServers": {
    "checked": {
      "type": "http",
      "url": "https://checked.be/mcp",
      "headers": { "X-Api-Key": "your-key" }
    }
  }
}

Key name and nesting differ per client (some use servers rather than mcpServers); the URL, the transport type and the header are what matter.

Tools
search_companies
query, page
Resolve a name, enterprise number, VAT number or NACE code to companies. Ten hits per page, each with its canonical dossier URL. Start here, then feed enterprise_number to the other tools.
get_company_profile
kbo
Identity, status, seat, NACE, headline figures from the latest filed annual accounts, financial-health score and bankruptcy-probability band. Banks, insurers and holdings get explicit not-applicable fields instead of misread trading ratios.
get_company_acts
kbo, limit
The differentiator: the verified, per-act extracted gazette timeline. Typed facts with resolved actor names and amounts, every fact stamped with its numac. Newest first, 1 to 100 acts.
get_company_officers
kbo
Board and mandates with currency semantics: current, stale (open per the register but not re-confirmed recently) or former, plus corporate officers, permanent representatives and statutory auditors. Never includes birth dates or national numbers.
get_company_financials
kbo, year
Parsed NBB figures and derived ratios per fiscal year, sector-aware. The free scope returns the latest fiscal year only; older years are gated and the tool says so in its note rather than returning silence.
get_extraction_status
kbo
How many gazette acts are on file for this company and how many have been extracted, plus the queue state. Call it whenever an act timeline looks empty or short.

Every tool result carries a source attribution string and, where relevant, a dossierUrl a human can open. National register numbers are stripped from every field before serialization.


Machine surfaces that need no key
GET /healthzLiveness. Always 200, no dependencies, never cached
GET /health/statusReadiness: database, Argus views and job-server probes plus data freshness. 200 even when degraded
GET /versionRelease id, commit hash and build timestamp of the running build
GET /sitemap.xmlSitemap index; sub-sitemaps are crawl-priority ordered with real lastmod dates
GET /robots.txtCrawler policy, including the named AI-crawler sections
GET /insolventies/rss.xmlNewest insolvency publications as RSS 2.0
GET /badge/{kbo}.svgEmbeddable status badge for one company
GET /export/company/{kbo}/financials.csvParsed annual-account lines for one company as CSV
GET /kbo/{kbo}301 to the canonical dossier URL. Handy for deep links when you only hold a number

Changelog

Breaking changes get a new path version and are announced to every key holder by mail before they ship. The one exception is a field that was stating something we could not measure: correcting it to null lands in v1, because leaving a wrong value in place for a version cycle is worse than the parser change. It is announced by mail all the same and listed below. Additive changes (new fields, new MCP tools) land in v1 and are listed here, so write tolerant parsers.

2026-08-09 Coverage is now in band, and absence is no longer reported as zero. New coverage, as_of and register_situation_code fields on the dossier. insolvency_event_count, admin_event_count and mandates are now nullable and come back null when the timeline read failed, where they used to be 0, 0 and []. score_out_of100 is nullable and is null for an unscored company, where it used to be 0 (a value that means "worst possible" on a scale where higher is safer); probability and altman_z are withheld with it. /lifecycle and /rollup return 503 instead of an empty array or a "nothing on file" 404 when the source did not answer. Widen your types before you upgrade.
2026-08-09 REST error responses now always carry a JSON body: 404 and 429 used to return an empty body, which the site's not-found handler turned into an HTML page. No field or route changed. First publication of this reference.
2026-07 MCP endpoint live with six read-only tools, key-gated, 60 requests per minute per key.
2026-06 /api/v1 live: dossier, lifecycle, rollup and search, key-gated.

Need a key, higher limits or bulk access?
Tell us what you are building. Keys are issued by hand, which means you get a human answer rather than a form.
enterprise@checked.be

Short marketing overview of the same API: the API page. Where the data comes from: sources. How we score: methodology. Browse the corpus: company search.