KYB Status Verification¶
Know Your Business (KYB) workflows need one question answered fast: is this counterparty currently in good standing with their national registry? This guide shows you how to answer that — for one company or thousands — and how to handle the case where the answer isn't a clean yes/no.
What FirmaDB returns¶
Every company record carries a normalized status plus a status_active boolean and a data_freshness block telling you how recently the value was confirmed against the source.
status |
status_active |
Meaning |
|---|---|---|
active |
true |
Trading entity in good standing |
dissolved |
false |
Wound up, struck off, liquidated |
suspended |
false |
Trading temporarily suspended (e.g., admin penalty) |
unknown |
false |
Status field not populated for this record |
unknown is NOT active
If status_active is false and status is unknown, FirmaDB has the record but no usable status flag for it. Do not silently treat this as a pass — escalate to manual review using the source_url.
Always preserve these three fields end-to-end into your KYB output:
statusandstatus_active— the decision itselfdata_freshness.last_confirmed_at— when FirmaDB last reconciled with the registrysource_url— the original government page, for the auditor
Single-counterparty check¶
For one entity, hit the lookup endpoint and read the status fields.
import httpx, os
resp = httpx.get(
"https://api.firmadb.com/v1/companies/GB/00445790",
headers={"Authorization": f"Bearer {os.environ['FIRMADB_API_KEY']}"},
)
company = resp.json()
if company["status_active"]:
print(f"{company['name']}: ACTIVE (confirmed {company['data_freshness']['last_confirmed_at']})")
else:
print(f"{company['name']}: {company['status'].upper()} — verify at {company['source_url']}")
const company = await fetch(
"https://api.firmadb.com/v1/companies/GB/00445790",
{ headers: { Authorization: `Bearer ${process.env.FIRMADB_API_KEY}` } }
).then(r => r.json());
const verdict = company.status_active
? `ACTIVE (confirmed ${company.data_freshness.last_confirmed_at})`
: `${company.status.toUpperCase()} — verify at ${company.source_url}`;
console.log(`${company.name}: ${verdict}`);
A typical response (trimmed to the KYB-relevant fields):
{
"country": "FR",
"registry_id": "552120222",
"name": "SOCIETE GENERALE",
"status": "active",
"status_active": true,
"dissolution_date": null,
"source_url": "https://annuaire-entreprises.data.gouv.fr/etablissement/55212022200013",
"data_freshness": {
"last_confirmed_at": "2026-05-04T03:14:22Z",
"freshness_class": "fresh"
}
}
MCP shortcut: verify_status¶
If you're calling from an agent, firmadb_verify_status returns a compact projection of just the KYB-relevant fields — same data, smaller payload, easier for the model to reason about. See MCP Setup.
Batch screening¶
For periodic recertification of an entire vendor list, use the batch endpoint and iterate over the per-row results. The pattern is identical to CRM enrichment — only the post-processing differs.
import httpx, os, uuid
API = "https://api.firmadb.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['FIRMADB_API_KEY']}"}
vendors = [
{"country": "FR", "registry_id": "552120222"},
{"country": "GB", "registry_id": "00445790"},
{"country": "BE", "registry_id": "0403.227.515"},
]
resp = httpx.post(
f"{API}/companies/lookup-batch",
json={"references": vendors, "idempotency_key": str(uuid.uuid4())},
headers=HEADERS, timeout=60,
).json()
review_queue = []
for row in resp["results"]:
if row["match_status"] != "found":
review_queue.append({"input": vendors[row["index"]], "reason": "not_found"})
continue
co = row["company"]
if co["status"] == "unknown" or not co["status_active"]:
review_queue.append({
"name": co["name"],
"status": co["status"],
"verify_at": co["source_url"],
})
print(f"{len(review_queue)} of {len(vendors)} vendors need manual review")
Data freshness¶
Every record carries a data_freshness block:
"data_freshness": {
"last_confirmed_at": "2026-05-04T03:14:22Z",
"freshness_class": "fresh",
"source_refresh_cadence": "weekly"
}
freshness_class |
Age | Use for KYB? |
|---|---|---|
fresh |
Confirmed within the last cadence window | Yes |
stale |
Past one cadence window | Treat as advisory; re-fetch before final decision |
unknown |
No timestamp recorded | Treat as stale |
Compliance reviewers want timestamps
Surface last_confirmed_at next to every status verdict in your UI. "Active as of 4 May 2026" reads better in an audit log than a bare "Active" — and it's often the first thing a reviewer asks for.
Most national registries refresh weekly. A handful (e.g. BE) push daily diffs; some (e.g. CY, MD) refresh monthly. See Country Coverage for per-country cadence.
Dissolved entities¶
When a company is dissolved, FirmaDB returns the dissolution date in dissolution_date. Use it to distinguish a long-defunct shell from a recent winding-up.
{
"name": "EXAMPLE TRADING LTD",
"status": "dissolved",
"status_active": false,
"dissolution_date": "2024-09-12",
"source_url": "https://find-and-update.company-information.service.gov.uk/company/12345678"
}
For recently-dissolved entities (within 90 days), most KYB policies require a closer look — assets may still be in transit and obligations may still be enforceable.
When you can't get a clean answer¶
unknown, stale, or not_found are all signals to escalate, not block-and-move-on. Your code should:
- Capture the input row and the FirmaDB response together (so the auditor can reconstruct what was checked).
- Surface
source_urlprominently so a human can verify against the registry directly. - Keep a queue separate from your "passed" list. Don't fail-open into the active set.
What's next¶
- Larger lists, more fields → Enrich a CRM List
- Coverage and field availability per country → Country Coverage
- Error handling and recovery → Errors