Skip to main content

Multi-Country Registry Services

Trust Relay integrates with 14 national business-registry providers across Europe (BE, NL, CH, CZ, EE, FR, DK, NO, FI, PL, LT, GB, RO, SK) to retrieve company data during OSINT investigations. Each registry provider follows a common interface pattern but implements country-specific API protocols, authentication schemes, and data extraction logic. The country routing system automatically dispatches investigations to the correct provider based on the target company's jurisdiction.

Provenance varies by source — most are official-register APIs, but some reach the official register only by HTML scraping, and a few are third-party mirrors or commercial aggregators rather than the register itself. See Registry Data Provenance & Coverage for the per-source tiering, live-verification evidence, and API-key provisioning status.


Coverage at a Glance

"Country coverage" means three different things in this system, and they carry different numbers. Conflating them overstates coverage, so this page states each one explicitly.

LayerCountWhat it meansSource of truth
Live registry integration14A dedicated national-registry provider that fetches authoritative company data (identity, directors, financials).app/agents/country_registry.py
Declared compliance-signal capability7Countries a compliance detector (shell, adverse-history, financials, etc.) can actually assess. A country not declared for a signal is honestly reported "not assessed for X", never silently cleared.app/services/country_capability.py (ADR-0068)
EU-27 introspection scope27The set of member states the capability registry knows about. Membership here means "in scope to be asked", not "assessed" — 20 of the 27 have no live registry and no declared signal capability.country_capability.py EU-27 frozenset
Pan-EU / country-agnostic sourcesn/aSources that apply to any jurisdiction: VIES (VAT), GLEIF (LEI), NorthData (fallback provider), OpenSanctions (sanctions/PEP), PEPPOL.app/services/

Honesty note. Marketing-level figures elsewhere ("32 countries", "23 integrations") count the EU-27 scope set and the per-source integration count (France alone is 3 sources, Czechia ~8). The authoritative operational number is 11 live registry countries; goAML export covers 8 jurisdictions (BE, CZ, DE, FR, LU, NL, RO, SK) and reasoning templates 6 (BE, FR, CZ, DE, NL, generic-EU) — DE and LU appear in those layers via the NorthData fallback, not a dedicated registry.

Provenance note. Coverage count is not coverage quality. Whether each source is the official register (via API or scraping) or a third-party mirror/aggregator — and which connectors have been verified live or still need an API key provisioned — is tracked in Registry Data Provenance & Coverage.

Registry data coverage (the 11 live countries)

✅ available · ⚠️ partial or degrades without an optional dependency · — not available

CountryIdentity / statusDirectorsFinancialsUBORegistry documentsAPI key required
🇧🇪 BE — KBO/BCE + NBB✅ (gazette)✅ (NBB CBSO)⚠️✅ (NBB filings)none
🇨🇿 CZ — ARES + ISIR + Justice.cz✅ (Sbírka listin)⚠️ curated¹✅ (Sbírka listin)none
🇪🇪 EE — Äriregister✅ (RIK avaandmed)none
🇨🇭 CH — Zefixnone
🇫🇷 FR — INPI + INSEE + BODACC✅ (INPI)✅ (INPI bilans)✅ (INPI actes)INPI creds + INSEE key
🇳🇱 NL — KvK✅ (jaarrekeningen)⚠️KvK key (hard-required)²
🇳🇴 NO — Brreg✅ (roller)✅ (Regnskaps.)none
🇩🇰 DK — CVR (cvrapi.dk)— owners only³⚠️ ownersnone (UA + email)
🇫🇮 FI — YTJ/PRH— not in open datanone
🇵🇱 PL — KRS— redacted⁵⚠️ filings only⁶none
🇱🇹 LT — JAR✅ status⁷— separate dataset⁷— separate dataset⁷none
🇬🇧 GB — Companies Housenames⚠️ accounts filed⁸PSCCompanies House key
🇷🇴 RO — ANAF + ONRC⚠️ BrightData⁴✅ (ANAF Bilant)BrightData token for directors
🇸🇰 SK — ORSR + RUZ✅ (ORSR scrape)✅ (RUZ)none

¹ CZ CNB / debarment / tax-advisor registers are static curated JSON by design (no public API, ADR-0042); the CZ UBO live path has been login-gated since 2025-12-17 and runs off a curated snapshot. ² NL is the only provider that hard-requires an API key — without kvk_api_key the provider logs a config error and returns no data. ³ The free cvrapi.dk returns owners/shareholders, not the board of directors (the full datacvr.virk.dk API needs a free service account — roadmap item). ⁴ RO directors come from ListaFirme.ro via BrightData and silently degrade to empty without brightdata_api_token (ANAF identity + financials are unaffected). ⁵ PL board-member names are redacted in the free KRS JSON extract (N**********); the provider reports board-composition counts and treats names as a documented gap (full names require the KRS PDF extract). ⁶ PL financials are surfaced as filing markers (which annual-statement years were filed), not figures — the numbers live in the separate free eKRS financial-document repository. ⁷ LT JAR is a linked-data register — the provider resolves legal status (bankruptcy/liquidation/removed) and legal form against enum datasets, but directors (valdymo_organai), financials (balanso_ataskaitos) and the registered address (buveines) live in separate JAR datasets not fetched in v1 (documented follow-up). UBO (JANGIS) is paid/access-gated post-Sovim. ⁸ GB is the keystone: Companies House publishes real officer names and — via the PSC register — genuine beneficial ownership with control bands (the only free public UBO source besides Latvia). Financials are iXBRL accounts (filing history), not figures-extracted in v1. A PSC-exempt company (a traded PLC) honestly reports "no active PSC" rather than a fabricated owner. Needs a free registered companies_house_api_key; absent → fail-closed "not assessed", never a silent skip.

Compliance-signal capability (ADR-0068)

Separate from data retrieval, each compliance detector declares which jurisdictions it can assess. Everything else is reported as an honest "not assessed for X" gap (fail-closed — never a silent clear).

SignalDeclared-supported countries
financial_statementsBE, CZ, EE, NL, NO, RO, SK
shell_detectionBE, CZ
geographic_concentrationBE, CZ
mailbox_detectionBE, CZ
virtual_office_detectionBE, CZ
adverse_corporate_historyBE, NL
address_canonicalisationBE
company_status_monitoringBE

Known depth gaps (honest, tracked). Four countries have a live registry but do not appear under any financial_statements-class signal — CH, FR, DK, FI (issue #473, resolved by verified-posture declaration). The financial_statements signal means one specific thing: structured financial FIGURES are extracted and assessable — not merely that financial-statement documents exist. app/services/country_capability.py now records a verified three-state posture per country (source of truth):

PostureCountriesMeaning
figures-extractedBE, CZ, EE, NL, NO, RO, SKa structured financial-health report is produced → assessable (the set above)
documents-onlyFR (INPI bilan attachment records retrieved; recent PDFs downloaded to evidence by the collection step), PL (filing-year markers parsed from the KRS response)statement documents/markers are actually retrieved but figures are not extracted → not assessable
not-retrievedCH, DK, FI (no free channel), GB, LT (channel exists but not fetched in v1)no financials obtained → not assessable

The bar for documents-only is actually retrieved, not available upstream: the FR registry agent retrieves the bilan attachment records (and the document-collection step downloads the recent PDFs to evidence), and PL parses real per-period filing records from the registry response — whereas GB's agent fetches only profile/officers/PSC and merely notes that Companies House publishes accounts, so GB is not-retrieved (the accounts endpoint exists but is not called in v1), exactly like LT. So FR is not under-declared — INPI retrieves the bilan PDFs as evidence, but no figures are parsed, so declaring FR financial_statements-assessable would be a false-capability claim (EU AI Act Art. 13 / AMLR Art. 28). Every non-assessable country carries an auditable reason (financials_coverage(country)); the assessable set is derived from the figures-extracted postures, so the two can never drift. Deepening a country (e.g. Docling-extracting the FR bilans, or fetching the GB accounts endpoint) is tracked and would flip its posture. Belgium remains the reference jurisdiction (all 8 signals).

Roadmap — candidate registries to add

Prioritised by (free-API feasibility × market size × EU-AML relevance). "Easy" = a free public JSON/REST API matching the existing pattern; verified against live endpoints 2026-07-19.

Poland (KRS) — integrated 2026-07-19 (pl_krs_service.py): free no-auth JSON API (api-krs.ms.gov.pl), large economy. Company identity/status/legal-form/NIP/REGON/address/share-capital/PKD are live; director names are a documented gap (redacted in the free JSON — footnote ⁵). ✅ Lithuania (JAR) — integrated 2026-07-19 (lt_jar_service.py): free no-auth Spinta JSON (get.data.gov.lt); completes the Baltics (EE+LV+LT) and covers the Olympic/OlyBet LT operating subsidiary. Identity + resolved legal status + legal form are live; directors/financials/address are separate-dataset gaps (footnote ⁷). ✅ United Kingdom (Companies House) — integrated 2026-07-19 (gb_companies_house_service.py), the keystone: real officer names + the PSC beneficial-ownership register (first free public UBO). Needs a free registered key (footnote ⁸).

PriorityCountryFeasibilityRationale
1🇮🇪 Ireland — CROEasyFree JSON API + daily CC-BY bulk; major EU holding/SPV hub (directors paywalled)
2🇸🇪 Sweden — BolagsverketEasyFree JSON identity API; directors/financials behind paid contract tier
3🇭🇷 Croatia — Sudski registarEasyFree JSON/XML; only cost is 6-hour OAuth token rotation
🇱🇻 Latvia — URMediumFree CC0 bulk incl. a rare free UBO feed; pair with LT for Baltic UBO
🇩🇪 DE / 🇪🇸 ESMedium (project)Big economies worth building despite friction (DE rate-capped portal; ES BORME event-stream)

Paid-only → keep on the NorthData fallback: IT, AT, PT, HU, LU (LU worth a paid LBR contract sooner if fund/holding entities recur).

UBO caveat (post-Sovim, CJEU 2022). Beneficial-ownership registers are access-restricted in almost every jurisdiction. Free public UBO exists in only two: the UK (PSC) and Latvia. Everywhere else, plan on OpenSanctions/aggregators or per-jurisdiction AML-entity onboarding for UBO depth — not the registries themselves.


Why Official Registries?

Commercial data aggregators (NorthData, OpenCorporates, Dun & Bradstreet) are useful for broad coverage but insufficient for compliance. Regulatory frameworks (6AMLD, AMLR) require that KYB/KYC decisions be traceable to authoritative sources -- the government registries where companies are legally incorporated.

Trust Relay's registry architecture provides:

  • Primary-source evidence: every company status, director appointment, and address comes directly from the national registry, creating an auditable evidence chain
  • Real-time verification: data is fetched live from registry APIs, not from stale aggregator snapshots
  • Corroboration scoring: registry data is cross-referenced with submitted information to detect discrepancies (name mismatches, dissolved companies, missing directors)
  • Graceful degradation: when a registry is unavailable or credentials are missing, the investigation continues with findings noting the gap

Provider Architecture


Country Router

The country router (backend/app/agents/country_registry.py) maps ISO 3166-1 alpha-2 country codes to provider instances. Each provider is a CountryRegistryProvider dataclass:

@dataclass(frozen=True)
class CountryRegistryProvider:
country_codes: tuple[str, ...] # e.g. ("BE",)
label: str # e.g. "Belgian Registry Agent"
source_label: str # e.g. "KBO/BCE" -- for evidence attribution
model_key: str # config key for LLM model (if applicable)
source_names: tuple[str, ...] # e.g. ("kbo", "gazette", "nbb")
investigate: Callable[..., Awaitable[RegistryAgentOutput]]
mock_flag: str = "" # config flag for mock mode

The registry is lazy-initialized on first lookup to avoid circular imports. Countries without a registered provider fall back to the default investigation path (NorthData aggregator).


Standardized Output

Every provider returns a RegistryAgentOutput regardless of the underlying API. This guarantees consistent downstream processing:

FieldTypeDescription
company_foundboolWhether the company was located in the registry
company_statusstrNormalized status: active, dissolved, inactive, not_found, unknown
directorslist[str]Director/board member names
directors_detailedlist[dict]Structured director data: name, role, mandate start date
uboslist[str]Ultimate beneficial owners (where available)
registered_addressdictNormalized address (street, postal_code, city, country)
legal_formstrLegal form code or description
nace_codeslist[str]Activity classification codes
financials_summarystrFinancial data availability note
findingslist[Finding]Structured findings with severity (VERIFIED, HIGH, MEDIUM, LOW)
corroboration_summarydictCounts: fields_confirmed, fields_discrepant, fields_unverifiable
discrepancieslistDetected data conflicts

Registry Coverage

BE -- Belgium: KBO/BCE

Provider: backend/app/agents/belgian_agent.py (composite agent)

The Belgian provider is the most comprehensive, combining three data sources through an AI-orchestrated agent:

SourceDataAuthentication
KBO/BCE (Crossroads Bank for Enterprises)Company registration, status, NACE codes, establishmentsPublic API
Belgisch Staatsblad / Moniteur BelgeGazette publications: director appointments, capital changes, statutesWeb scraping
NBB/CBSO (National Bank of Belgium)Annual financial filingsPublic API

This is the only provider that uses an LLM-orchestrated agent (model_key: "belgian_agent_model") because it synthesizes across multiple sources. All other providers are deterministic HTTP clients.


NL -- Netherlands: KvK (Kamer van Koophandel)

Provider: backend/app/services/registries/nl_kvk_service.py

AspectDetail
APIhttps://api.kvk.nl/api/v1/basisprofielen/{kvkNummer}
AuthenticationAPI key (apikey header) -- register at developers.kvk.nl
Identifier8-digit KvK number
Data returnedCompany name, legal form (rechtsvorm), founding date, SBI activity codes, visiting address
DirectorsAvailable via eigenaar (owner) and bestuurders (directors) fields
UBOsNot available -- requires separate KvK UBO registry lookup

CH -- Switzerland: Zefix (Zentraler Firmenindex)

Provider: backend/app/services/registries/ch_zefix_service.py

AspectDetail
APIhttps://www.zefix.admin.ch/ZefixREST/api/v1
AuthenticationNone (public API)
IdentifierSwiss UID (CHE-xxx.xxx.xxx) or company name
Data returnedCompany name, legal seat, status, UID, CHID, purpose, legal form
DirectorsAvailable via /company/{chid}/persons endpoint
Lookup strategyUID lookup first, fallback to name search

CZ -- Czech Republic: ARES + ISIR + Justice.cz (multi-source)

Provider: backend/app/services/registries/cz_ares_service.py (entry point run_cz_agent)

The Czech provider orchestrates three official sources in parallel via asyncio.gather(): ARES for company identity, ISIR for insolvency proceedings (cz_isir_service.py), and Justice.cz for corporate documents/sbírka listin (cz_justice_service.py). Additional CZ-specific services exist for downstream enrichment: cz_cnb_service.py (CNB financial-market entity register), cz_debarment_service.py, cz_kdp_service.py (tax-advisor register), cz_ubo_service.py (beneficial-owner register), and the cz_financials_extractor.py / cz_bank_financials_extractor.py extractors.

AspectDetail
API (ARES)https://ares.gov.cz/ekonomicke-subjekty-v-be/rest/ekonomicke-subjekty/{ico}
AuthenticationNone (public APIs)
IdentifierICO (Czech registration number)
Data returnedCompany name (obchodniJmeno), registered address (sidlo), legal form, founding date, NACE codes
DirectorsAvailable via statutarniOrgan.clenove -- natural persons or legal entities
InsolvencyISIR insolvency register checked in parallel (cz_isir_service.fetch_insolvency)
DocumentsJustice.cz sbírka listin enumerated in parallel (cz_justice_service.fetch_justice_documents)

EE -- Estonia: Ariregister (Estonian Business Register)

Provider: backend/app/services/registries/ee_ariregister_service.py

AspectDetail
APIhttps://ariregister.rik.ee/est/api/autocomplete + /est/company/{reg_code}/json
AuthenticationNone (public API)
IdentifierEstonian registration code
Data returnedCompany name (arinimi), status, address, registration code
DirectorsAvailable via isikud (persons) array with role and mandate dates
Lookup strategyDirect detail fetch by reg code, fallback to autocomplete search

FR -- France: INPI RNE + INSEE Sirene + BODACC (3-source)

Orchestrator: backend/app/services/registries/fr_insee_service.py

The French provider is one of several multi-source providers (alongside Belgium, Czech Republic, Romania, and Slovakia), orchestrating three official data sources in parallel via asyncio.gather(). INPI is primary for directors and documents, INSEE for status and legal form, BODACC for lifecycle events. If any source fails, the investigation degrades gracefully and reports the gap.

Source 1: INPI RNE (Registre National des Entreprises)

Service: backend/app/services/registries/fr_inpi_service.py

AspectDetail
APIhttps://registre-national-entreprises.inpi.fr/api/companies/{siren}
AuthenticationBearer token -- POST /api/sso/login with email/password (free account at data.inpi.fr)
Data returnedCompany identity, directors (composition), share capital, corporate purpose (objet social), legal form, establishments
DocumentsArticles of association (actes), financial statements (bilans) -- downloadable as PDF via /api/actes/{id}/download
Rate limit10,000 requests/day, 10 GB/day transfer
LicenseLicence Ouverte Etalab 2.0 (free, commercial use)
ConfigINPI_USERNAME (email), INPI_PASSWORD

INPI is the primary source for director data -- previously unavailable through the public Sirene API without government DataPass authorization. Documents (articles of association, financial statements) are enumerated via /api/companies/{siren}/attachments and can be downloaded as PDFs for Docling conversion.

Source 2: INSEE Sirene (enhanced)

Service: backend/app/services/registries/fr_insee_service.py (inline _fetch_insee)

AspectDetail
APIhttps://api.insee.fr/api-sirene/3.11/siren/{siren}
AuthenticationAPI key -- X-INSEE-Api-Key-Integration header (register at portail-api.insee.fr)
Data returnedCompany name (denomination), administrative status (active/dissolved), legal form code, APE/NACE activity code, creation date, HQ address, employee count bracket
Rate limit30 requests/minute
Special handling403 responses indicate confidentiality protection (avis de non-diffusion)
ConfigINSEE_API_KEY

INSEE remains the authoritative source for company status (active vs. dissolved) and legal form classification.

Service: backend/app/services/registries/fr_bodacc_service.py

AspectDetail
APIhttps://bodacc-datadila.opendatasoft.com/api/explore/v2.1 (dataset: annonces-commerciales)
AuthenticationNone -- open data API
Data returnedLifecycle events: company creations, modifications, dissolutions, sales/transfers, collective proceedings (insolvency, liquidation), account filing notices
LicenseLicence Ouverte Etalab 2.0
ConfigNone required

BODACC provides adverse event detection -- insolvency proceedings, liquidations, and restructurings are flagged as HIGH severity findings. Non-adverse announcements (name changes, address moves) are reported as VERIFIED context.

SIREN Identifier Parsing

The agent accepts multiple identifier formats and normalizes to a 9-digit SIREN:

Input formatExampleHandling
SIREN (9 digits)552032534Used directly
SIRET (14 digits)55203253400024Truncated to first 9 digits
FR VAT numberFR40552032534Strip FR prefix + 2-digit key, yielding 9-digit SIREN

Merge Strategy

FieldPrimary sourceFallback source
Company nameINPIINSEE
Status (active/dissolved)INSEE--
DirectorsINPI--
Legal form codeINSEEINPI
APE/NACE codeINSEEINPI
Creation dateINSEEINPI
AddressINPIINSEE
CapitalINPI--
Corporate purposeINPI--
Lifecycle eventsBODACC--
Documents (articles, bilans)INPI--

Document Auto-Retrieval

France was the first country with automatic document collection, and the collect_registry_documents Temporal activity now also handles BE (NBB filings) and CZ (Justice.cz sbírka listin) — see app/services/document_evidence_collector.py (country_upper in {"FR", "BE", "CZ"}; other countries degrade gracefully with no downloadable documents). For France, the activity downloads articles of association and financial statement PDFs from INPI and stores them in MinIO. Recency filtering (the last DOCUMENT_RECENCY_YEARS) avoids downloading decades of filing history. These documents feed into the dynamic document requirements system -- for French companies, the compliance officer may only need to request a Director ID from the end customer, since corporate documents are already available from the registry.


DK -- Denmark: CVR (Central Business Register)

Provider: backend/app/services/registries/dk_cvr_service.py

AspectDetail
APIhttps://cvrapi.dk/api?search={cvr_number}&country=dk
AuthenticationNone, but User-Agent header with contact email required by API terms of service
IdentifierCVR number
Data returnedCompany name, status, address, owners
DirectorsAvailable via owners array

NO -- Norway: Bronnoysundregistrene (Brreg)

Provider: backend/app/services/registries/no_brreg_service.py

AspectDetail
APIhttps://data.brreg.no/enhetsregisteret/api/enheter/{orgNr} + /roller
AuthenticationNone (public API)
IdentifierNorwegian organization number (orgNr)
Data returnedCompany name, status, business address (forretningsadresse), organization form
DirectorsAvailable via separate /roller endpoint

FI -- Finland: YTJ/PRH (Finnish Patent and Registration Office)

Provider: backend/app/services/registries/fi_ytj_service.py

AspectDetail
APIhttps://avoindata.prh.fi/opendata-ytj-api/v3/companies?businessId={y_tunnus}
AuthenticationNone (open data API)
IdentifierFinnish business ID (Y-tunnus)
Data returnedCompany name, registered office, status, business line
DirectorsNot available via open data API -- requires separate PRH portal access

SK -- Slovakia: ORSR (Obchodný register) + RUZ

Provider: backend/app/services/registries/sk_orsr_service.py (entry point run_sk_agent)

AspectDetail
Sourcehttps://www.orsr.sk -- Slovak Ministry of Justice Commercial Register (HTML scraping, windows-1250 encoding)
AuthenticationNone (public website)
IdentifierSlovak IČO (Identifikačné číslo organizácie)
Lookup strategyTwo-step: search by IČO to find the vypis.asp detail link, then parse the detail-page HTML table
Data returnedLegal name, address, IČO, legal form, directors, business activities, company status
Status mappingNormalizes Slovak status labels (aktívna → active, v likvidácii → in_liquidation, vymazaná/zrušená → dissolved)
FinancialsRUZ (Register Účtovných Závierok) provides annual filings -- see Country Financial Scrapers below (sk_ruz_service.py)

ORSR is the company-identity source registered in the country router for country="SK"; RUZ supplies financial filings. Together they make Slovakia a multi-source provider (source_label: "ORSR/RUZ").


PL -- Poland: KRS (Krajowy Rejestr Sądowy)

Provider: backend/app/services/registries/pl_krs_service.py (entry point run_pl_agent)

AspectDetail
APIhttps://api-krs.ms.gov.pl/api/krs/OdpisAktualny/{krs}?rejestr=P&format=json
AuthenticationNone -- open-data API, licensed for commercial reuse
Identifier10-digit KRS number (leading zeros optional -- normalised via zfill(10))
Registerrejestr=P (entrepreneurs / companies); the associations register (S) is out of scope for KYB
Data returnedCompany name, legal form (formaPrawna), NIP + REGON, registered seat/address, share capital, PKD activity codes (kodDzial.kodKlasa.kodPodklasa), registration date, annual financial-statement filing markers
DirectorsRedacted in open data. Names arrive masked (N**********); the provider reports management/supervisory board counts and treats names as a documented gap — full names require the KRS PDF extract
StatusInferred from the legally-mandated name suffix: "w likwidacji" → in_liquidation, "w upadłości" → bankruptcy (KSH Art. 274 §2); otherwise active. The dzial6 history blob is deliberately not scanned — it narrates mergers/splits (sometimes of other entities) and would false-match on historical liquidation mentions
FinancialsFiling markers only (which years were filed); figures require the separate free eKRS financial-document repository

KRS is a strong company source but not a people source: the free JSON masks all natural-person names. This mirrors the Finland provider's directors-not-available posture — the investigation surfaces a rich company profile and an honest "board names redacted" gap rather than emitting masked strings that would pollute person matching.


LT -- Lithuania: JAR (Juridinių Asmenų Registras)

Provider: backend/app/services/registries/lt_jar_service.py (entry point run_lt_agent)

AspectDetail
APIhttps://get.data.gov.lt/datasets/gov/rc/jar/iregistruoti/JuridinisAsmuo?ja_kodas={code}
AuthenticationNone -- Spinta open-data REST, commercial reuse permitted (~5k req/h)
Identifier9-digit Lithuanian legal-entity code (juridinio asmens kodas)
Data returnedCompany name (ja_pavadinimas), registration date, resolved legal status and legal form
StatusResolved against the formos_statusai/Statusas enum — bankruptcy (codes 5/6/13), liquidation (7/9/11), removed/defunct (10/12) map to terminal statuses that trip the ADR-0065 onboarding gate; a deregistration date (isreg_data) is the fail-safe backstop
Legal formResolved against the formos_statusai/Forma enum (e.g. "Private Limited Liability Company" / UAB)
Directors / financials / addressHonest v1 gaps -- these live in separate JAR datasets (valdymo_organai, balanso_ataskaitos, buveines) not fetched in v1; disclosed in the output, not silently empty
UBONot available -- JANGIS is paid/access-gated post-Sovim

JAR is a linked-data register: a company record references its status and form by id, so the provider caches the two small enum datasets process-wide and resolves them per lookup. Validated live against UAB "Olympic Casino Group Baltija" (the OB Holding Lithuanian operating subsidiary) and Maxima LT. Completes Baltic coverage (EE + LV-planned + LT).


GB -- United Kingdom: Companies House

Provider: backend/app/services/registries/gb_companies_house_service.py (entry point run_gb_agent)

AspectDetail
APIhttps://api.company-information.service.gov.uk/company/{number} (+ /officers, /persons-with-significant-control)
AuthenticationFree registered key (companies_house_api_key) via HTTP Basic auth — key as username, empty password
Identifier8-char company number (8 digits, or a 2-letter prefix like SC/NI/OC + 6 digits)
Data returnedCompany identity, company_status, incorporation date, SIC codes, registered office; real director/officer names; PSC beneficial owners with control bands
StatusClean company_status string mapped fail-closed: dissolved/liquidation/administration/receivership/insolvency → terminal (trips the ADR-0065 gate); an unrecognised status → "unknown" (never silent active); has_insolvency_history/has_been_liquidated raise a finding even when currently active
DirectorsReal names from /officers (active officers in the flat list; resigned in the detailed list)
UBOThe keystone/persons-with-significant-control yields genuine beneficial owners with natures_of_control bands (ownership/voting %). Ceased PSCs excluded; a PSC-exempt traded company honestly reports "no active PSC"
FinancialsiXBRL accounts in the filing history — not figures-extracted in v1

Companies House is the only free registry that publishes both real officer names and beneficial ownership, so run_gb_agent populates directors and ubos for real, feeding the ownership engine. Fail-closed on the key: if companies_house_api_key is unset the lookup returns a "not assessed" finding, never a silent skip. Validated live against Revolut Ltd, Monzo Bank Ltd and Tesco PLC (the PLC correctly returning zero PSCs). GB is non-EU (post-Brexit) but appears constantly as a parent/holding node in European corporate structures.


Authentication Summary

PatternCountriesMechanism
Open accessCH, CZ, EE, DK, NO, FI, PL, LT, RONo authentication -- public APIs
HTML scrapingSKPublic website (orsr.sk), no API
API keyNL, FR (INSEE), GBapikey / X-INSEE-Api-Key-Integration header; GB = free key as HTTP Basic username
Bearer tokenFR (INPI)POST /api/sso/login with email/password, token returned in response
No authFR (BODACC)Open data API -- no credentials required
Composite (mixed)BEKBO public + gazette scraping + NBB public
Multi-source orchestrationBE, FR, CZ, RO, SKMultiple sources called in parallel, results merged

Corroboration Pattern

Every provider follows the same corroboration pattern when processing results:

  1. Name matching: compare the registry's official name against the submitted company name (case-insensitive substring match). Produces a VERIFIED finding on match or a MEDIUM severity finding on mismatch.
  2. Status check: if the company is dissolved, inactive, or struck off, emit a HIGH severity finding (serious compliance risk).
  3. Registration confirmation: if found and active, emit a VERIFIED finding with registration date and legal form.
  4. Director extraction: structured director data (name, role, mandate start) for downstream person validation and graph ETL.
  5. Summary scoring: corroboration_summary counts confirmed fields, discrepant fields, and unverifiable fields (typically UBOs).

Error Handling

All providers implement consistent error handling with graceful degradation:

ScenarioBehavior
API timeoutLOW severity finding, company_status: "unknown"
HTTP errorLOW severity finding with status code
Company not foundHIGH severity finding, company_status: "not_found"
Missing credentialsMEDIUM severity finding with registration instructions
Missing identifierMEDIUM severity finding with format guidance

The investigation never fails due to a registry being unavailable. Findings document the gap, and the officer sees exactly which verifications could not be completed.


Integration with Investigation Pipeline

Registry providers are invoked during the OSINT investigation phase (Temporal activity run_osint_investigation):

Registry data flows into:

  • Investigation findings for the officer review dashboard
  • Knowledge graph via GraphETL (Company, Person, Address nodes)
  • Canonical entities for goAML export (persons, addresses)
  • Risk engine for automated risk scoring (dissolved company = elevated risk)

VAT Auto-Derivation

For several European countries, the VAT number can be derived deterministically from the company registration number using country-specific algorithms. Trust Relay implements these derivations to populate VAT fields automatically without requiring an additional API call.

CountryAlgorithmExample
FR (France)SIREN (9 digits) → VAT: "FR" + key + SIREN where key = (12 + 3 × (SIREN % 97)) % 97SIREN 123456789FR…123456789
BE (Belgium)Enterprise number (10 digits) → VAT: prepend "BE"0123456789BE0123456789
IT (Italy)Registration number (11 digits) is already the partita IVA base; prepend "IT"12345678901IT12345678901
ES (Spain)NIF/CIF (≥8 chars) → VAT: prepend "ES"A12345678ESA12345678
PT (Portugal)NIPC (9 digits) → VAT: prepend "PT"123456789PT123456789
PL (Poland)NIP (10 digits) → VAT: prepend "PL"1234567890PL1234567890
CZ (Czech Republic)IČO (8 digits) → DIČ: prepend "CZ"14706725CZ14706725

Countries where the VAT number cannot be derived from the registration number (require a separate VAT registration and an explicit vat_number): DE, NL, LU, AT, SK, HU, RO, DK, SE, FI, IE, GB.

The derived VAT number is later validated against the VIES API before being stored. A VIES isValid: false response generates a finding; a validation timeout falls back gracefully without blocking the investigation.

Code note: the derivation logic currently lives inline in the case-creation router, not in a standalone service module. The function is _derive_vat_number(registration_number: str, country: str) -> str | None in backend/app/api/case_crud.py (called from the _run_pre_enrichment path). There is no app/services/vat_derivation.py.


Country Financial Scrapers

In addition to company registration data, Trust Relay retrieves financial filing data directly from national registries for four additional countries:

NO — Norway: Regnskapsregisteret (Brreg)

The no_brreg_service.py provider extends its basic company lookup with a call to the Brønnøysund Register Centre's accounting register:

AspectDetail
Endpointhttps://data.brreg.no/regnskapsregisteret/regnskap/{orgNr}
Data returnedRevenue, profit/loss, total assets, equity, employee count (from the most recent filed annual accounts)
AuthenticationNone (public API)

Financial metrics are extracted from the resultatregnskapResultat / eiendeler / egenkapitalGjeld sections (revenue from driftsinntekter/sumDriftsinntekter, profit from aarsresultat) and mapped to the FinancialHealthReport model (fetch_financials in no_regnskapsregisteret.py).

RO — Romania: ANAF + ONRC

Provider: backend/app/services/registries/ro_onrc_service.py (composite agent)

Romania is one of several multi-source providers (alongside Belgium, France, Czech Republic, and Slovakia), combining three data sources:

SourceService FileDataAuthentication
ANAF PlatitorTvaRest v9ro_anaf_company_service.pyCompany name, structured address, legal form, CAEN code, registration status, VAT status, fiscal inactivity, e-Factura statusPublic API (no auth)
ANAF Bilantro_anaf_service.py3-year financial statements (assets, equity, revenue, profit, employees)Public API (no auth)
ONRC via BrightDataro_onrc_service.pyDirectors/administrators, shareholders, mandate datesBrightData MCP scraping of ListaFirme.ro

All three sources are queried in parallel via asyncio.gather(). The ANAF endpoints share a module-level asyncio.Lock rate limiter enforcing 1 request/second across all ANAF traffic.

ANAF PlatitorTvaRest v9 is the primary company identity source:

  • Endpoint: POST https://webservicesp.anaf.ro/api/PlatitorTvaRest/v9/tva
  • Request: [{"cui": 12345678, "data": "2026-04-05"}]
  • Returns: company name (denumire), trade registry number (nrRegCom), structured HQ address (adresa_sediu_social), legal form code (forma_juridica — resolved via ro_nomenclator.py), CAEN activity code, VAT registration history, fiscal inactivity status, and e-Invoice registry status
  • Legal form codes are mapped to human-readable strings (e.g., 40 → "SRL", 41 → "SA") via a nomenclator lookup table sourced from ANAF's official nomenclator page

BrightData director scraping uses the scrape_as_markdown MCP tool to extract directors from listafirme.ro/firma/{cui}. This is optional — when BrightData is unavailable or in mock mode, the agent returns empty directors and the investigation continues with a gap finding. NorthData provides fallback director data when available.

Key difference from Belgium: Romania has no official ONRC API for directors — ONRC requires a paid subscription contract. BrightData scraping of the public ListaFirme.ro aggregator is the pragmatic workaround. This may be replaced by a direct ListaFirme API integration in the future.

SK — Slovakia: RUZ (Register Účtovných Závierok)

Provider: backend/app/services/registries/sk_ruz_service.py (financials companion to the ORSR registry provider documented above)

AspectDetail
APIhttps://www.registeruz.sk/cruz-public/api/uctovne-zavierky?ico={ico}
AuthenticationNone (public API)
IdentifierSlovak ICO (Identifikačné číslo organizácie)
Data returnedAnnual account filings list; detail endpoint returns revenue, assets, equity per year

The service fetches the latest filing year and extracts key financial metrics for FinancialHealthReport. Company-identity data (name, directors, status) comes from the ORSR provider — see the SK registry coverage section above.

NL — Netherlands: KvK Open Dataset

Provider: backend/app/services/registries/nl_kvk_service.py (extended)

In addition to the authenticated Basisprofielen API, the NL provider queries the KvK Open Dataset for SBI (Standard Industrial Classification) code descriptions and historical name changes. The Open Dataset endpoint does not require authentication and complements the paid API data.


EE Äriregister — Autocomplete Fix

The Estonian Business Register autocomplete endpoint (/est/api/autocomplete) was updated to correctly parse responses that arrive with Content-Type: text/html despite containing valid JSON. The service now attempts JSON parsing regardless of the declared content type, falling back to HTML parsing only if JSON parsing fails.

Additionally, the autocomplete response structure wraps results in a {"items": [...]} envelope rather than returning a bare array. The parser now checks for both formats to remain compatible with future API changes.


NorthData Improvements

Several reliability improvements were made to the NorthData integration (backend/app/services/northdata_service.py):

_as_list Helper

NorthData's API inconsistently returns single-item collections as either a bare object or a {"items": [...]} wrapped array. The _as_list helper now handles all three cases: bare object → [obj], {"items": [...]} envelope → inner list, bare array → as-is. This prevents silent data loss when a company has exactly one related company or one financial year.

_extract_relation_name Unwrapping

Relation entries can nest the actual company or person name inside a company or person sub-key. The _extract_relation_name function now unwraps these containers before attempting name extraction, preventing None values in the relatedCompanies output.

euId Extraction

The relatedCompanies response now includes the NorthData euId for each connected entity. This identifier is extracted and passed downstream to the EVOI network scan as the registration ID for subsequent NorthData lookups, significantly improving subsidiary and affiliate match quality (especially for entities with common trade names).

Global Rate Limiter

A module-level asyncio.Lock-based rate limiter enforces a 2-second minimum interval between all NorthData API requests. This rate limiter is shared across all concurrent coroutines — including the recursive network scan — preventing HTTP 429 responses and ensuring the integration remains within NorthData's stated API terms.


Adding a New Country

To add a new country provider:

  1. Create backend/app/services/registries/<cc>_<registry>_service.py with a run_<cc>_agent() function matching the CountryRegistryProvider.investigate signature
  2. Return a RegistryAgentOutput with findings, directors, corroboration summary
  3. Register the provider in backend/app/agents/country_registry.py inside _init_registry()
  4. Add any required API credentials to backend/app/config.py (pydantic-settings)
  5. Add tests under backend/tests/ (e.g. test_countries.py, test_country_registry.py)

The standardized interface means no changes are needed to the OSINT agent, graph ETL, or risk engine -- they consume RegistryAgentOutput regardless of which country produced it.