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.
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.
| REST base URL | https://checked.be/api/v1 |
| MCP endpoint | https://checked.be/mcp |
| Authentication | X-Api-Key request header, on both surfaces |
| Format | JSON. REST fields are snake_case; MCP tool results are camelCase (the two serializers differ, this is not a typo) |
| Methods | Read-only. Every documented endpoint is a GET; MCP is JSON-RPC over POST but exposes no write tool |
| Rate limit | 60 requests per minute (see the rate-limit section, the two surfaces count differently) |
| CORS | Not enabled. Call from your server, not from a browser |
| Caching | Every /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 |
| OpenAPI | None published. This page is the specification |
| Versioning | In the path (/v1/). Additive changes ship without a version bump; see the changelog |
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.
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.
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"
}
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": "..." }
]
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
}
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 }
]
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 |
REST /api/v1/* | 60 requests per minute, counted per client IP, fixed window |
MCP /mcp | 60 requests per minute, counted per API key when a key is present, otherwise per IP |
| Over the limit | 429 with Retry-After: 60 |
| Repeated bad keys | More 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.
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.
| 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.
| GET /healthz | Liveness. Always 200, no dependencies, never cached |
| GET /health/status | Readiness: database, Argus views and job-server probes plus data freshness. 200 even when degraded |
| GET /version | Release id, commit hash and build timestamp of the running build |
| GET /sitemap.xml | Sitemap index; sub-sitemaps are crawl-priority ordered with real lastmod dates |
| GET /robots.txt | Crawler policy, including the named AI-crawler sections |
| GET /insolventies/rss.xml | Newest insolvency publications as RSS 2.0 |
| GET /badge/{kbo}.svg | Embeddable status badge for one company |
| GET /export/company/{kbo}/financials.csv | Parsed 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 |
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. |
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.