Global Company Registry Search API — 11 Countries, Officers & LEI
Design notes for an Actor that searches the official company registers of 11 countries by name or registration number and returns one normalized, source-cited KYB record per entity — no API key anywhere in the path.
Design notes for global-company-registry-search, an Apify Actor that searches the official company registers of eleven countries — by company name or by registration number — and returns one normalized record per matched entity, each carrying the URL of the register entry it was read from. It exists for the cross-border KYB question that currently costs a dozen separate integrations to answer.
What this is
Give it a query — or a queries list of up to a hundred names and numbers, so one run verifies a whole supplier list — and, optionally, a list of ISO country codes. The query can be a company name (Equinor) or an official registration number — a UK company number, a French SIREN or SIRET, a Norwegian organisasjonsnummer, a Finnish business ID, a Swiss UID, an Estonian or Czech registry code, a Latvian regcode, a Polish KRS number, an Australian ABN or ACN, a Singapore UEN. Every requested country is searched concurrently, and each matched entity comes back with the same field names regardless of which register produced it: legal name, registration number, status (verbatim in the source’s own language and mapped to active / inactive / unknown), legal form, incorporation date, registered address as one line plus its components, activity codes tagged with the source’s own classification scheme, officers where the register publishes them, a GLEIF Legal Entity Identifier where one confidently matches, and source_url.
Coverage today is Companies House (UK), Annuaire des Entreprises (France, over SIRENE and INPI data), Brønnøysundregistrene’s Enhetsregisteret (Norway), the Finnish Patent and Registration Office’s open data (PRH / YTJ), Zefix (Switzerland), the e-Business Register (Estonia), Uzņēmumu reģistrs (Latvia), ARES (Czech Republic), KRS (Poland), the Australian Business Register (ABN Lookup), ACRA (Singapore), and Registrų centras (Lithuania) — with the GLEIF Global LEI Index layered over all of them for enrichment. Ten of those support both name search and number lookup. Poland is number-only. Lithuania is in the list but not searchable in this version, and says so on every run rather than quietly disappearing.
The decision the Actor makes on each call is smaller than it looks and is the part most worth explaining: for every country independently, does this query look like one of THIS country’s registration numbers? If yes, it is a number lookup; if no, a name search. That per-country framing is not fussiness. A nine-digit query is simultaneously a valid French SIREN, a Norwegian organisasjonsnummer, a Lithuanian legal-entity code and an Australian ACN, so “which country is this number from?” has no answer, while “is this one of yours?” always does. Guessing wrong costs a caller nothing worse than a name search, which is the broader query anyway.
The output shape has one more property worth stating plainly: every country that produced no entity still produces a row. Not an omission — a row, in the same schema, carrying a machine-routable outcome and a plain-language message. A caller asking about every covered country gets one answer per country, and none of the non-delivery rows is charged.
Why I built it this way
Primary sources, keyless, or not at all
The obvious way to build cross-border company data is to buy it. One vendor API, one schema, coverage already assembled. I didn’t, for a reason that shows up the first time somebody asks where a field came from: an aggregator’s record is a copy of the register on the aggregator’s refresh cycle, and you cannot cite it. In a diligence file, a supplier-onboarding audit trail, or a sanctions-adjacent screening workflow, the citation is a substantial part of the value. “Our vendor said the company is active” and “the Norwegian register says the company is Registrert, here is the entry” are different artifacts.
So every source in the coverage list is the country’s own official register, read directly, with no API key and no account anywhere in the path. That constraint did real work during selection. It eliminated sources that would have widened the map — several European registers are behind paid access, credentials, or reuse terms that forbid systematic retrieval, and those are simply absent rather than worked around. It also eliminated the shape of build where a key or a login sits between the caller and the data, because a keyless Actor is one a caller can evaluate in a single run with nothing to provision.
The second constraint was: no browser. Every adapter is plain HTTP against a published endpoint or an ordinary public page. That is what keeps an all-countries query inside a few dozen seconds and keeps the per-lookup cost low enough that “search everywhere” is a reasonable default rather than a luxury. It is also, as it turns out, the constraint that decided Poland and Lithuania — more on that below.
Officers only where the register publishes them
Officers and directors are the field people ask for most, and the one I deliberately did least with. They are returned from exactly three registers: Companies House’s officers page, the dirigeants published by Annuaire des Entreprises, and the roller endpoint at Brønnøysundregistrene. Each of those publishes officer names under its own open licence. Everywhere else officers comes back as an empty list with an officers_note explaining which case applies.
That line is a licensing line, not a capability gap, and drawing it cost coverage on purpose. Two registers in the list expose officer-ish data that I do not mine: Singapore’s licence expressly excludes personal data, and while the Polish register does publish board representation, its dataset reuse terms were never established to my satisfaction. Both would need a terms ruling before anything changes, and until then the honest answer is an empty list with a note.
Inside the three that do publish, the returned fields are narrower than the source: name, role, and the appointment date the register itself states. Never dates of birth — two of those three registers expose them and I drop them. Never phone numbers, never email addresses. The field notes on a record say which case produced an empty list, because a truncated-looking record invites a caller to assume a pipeline bug and retry, and that is a bad outcome for everyone.
There is one honest limit inside the coverage I do have: the UK officers page paginates, and it lists current and resigned appointments together. A company with more appointment records than one page returns a partial roster. That is documented rather than papered over.
Why some countries are number-only, and one is deferred
Poland and Lithuania are the two rows in the coverage table that don’t behave like the rest, and both are instructive about what “coverage” should mean.
Poland’s National Court Register serves entity lookups by KRS number through an open interface, verified and in production. Its name-search front end is a different system, and it refuses an automated request on every path and method I probed; the number-lookup interface exposes no name endpoint at all. I had two options: leave Poland out, or ship the half that works. Shipping the half means a Polish number query returns a complete record, and a Polish name query returns a free row that names the limitation and tells the caller to supply the ten-digit KRS number instead. That is more useful than absence, because the caller who has the number gets the record and the caller who doesn’t learns exactly what to do next.
Lithuania is the harder case. It cleared reconnaissance at a browser tier and then, re-probed at build time over plain HTTP, answered with an interactive challenge. Serving it would mean adding a browser to an Actor whose entire cost profile depends on not having one — for a register that also caps public use at a hundred searches a day. So Lithuania is deferred, and the distinction I care about is that it stays in the coverage list and answers with a free row explaining itself. A default run that silently omitted Lithuania would leave a caller unable to tell “we cannot search this yet” from “your country code was wrong.” Those are different answers and the output should not conflate them.
Misses are explained, not fixed
The design rule I’d defend hardest: when a country produces nothing, the Actor explains it in prose rather than trying to rescue it.
Concretely, there are six non-delivery outcomes — no_match, not_covered, failed, rate_limited, invalid_input, internal_error — and every one of them arrives as a real dataset row with a real sentence in message. A 429 from a register surfaces honestly as rate_limited for that country, with no in-run retry, while the other eleven countries keep delivering. A register that goes down becomes failed for that country only. An unusable input becomes one invalid_input row on a run that still succeeds. None of these is charged, and the run never ends FAILED — because a failed run hands an agent caller an exception where it wanted an answer, and it is penalised in Actor quality for no benefit to anybody.
The prose matters as much as the taxonomy. A no_match from Switzerland appends the fact that the Swiss open dataset lists currently-registered entities only, so a dissolved company will not appear there even if it once existed. A no_match from Singapore notes that the dataset is a monthly snapshot. Latvia’s notes that its dataset carries branches and terminated entities as separate rows, so one name can legitimately match several records. Australia’s notes that the register publishes ABN-level details rather than a full company record, which is also why Australian records carry no incorporation date — the register publishes a registration date, and passing that off as an incorporation date would be a small, quiet lie.
That is the general principle: where a register’s semantics differ, the record says so, instead of the schema smoothing it into something uniform and wrong. The same instinct governs status. It is kept verbatim, in the source’s own language, and mapped to three values — because comparing entities across countries needs the mapping, and defending a conclusion needs the original wording.
One more consequence of refusing to fix things: result relevance is the register’s own. The Swiss federal linked-data endpoint the Zefix data is read from offers no relevance function, so a broad name search there orders by entity identifier and a small per-country cap would surface an obscure substring match ahead of the obvious company. That turned out to be too costly to leave alone once records are billed individually, so the Actor does impose one ordering of its own, and says so here: within a country, name-search candidates are ranked by how the register’s name matches the query — exact, then prefix, then whole-word, then anything else — before the per-country cap is applied, and nothing is dropped. Number lookups are never reordered. For the Swiss register the same preference is expressed inside the query itself, because that endpoint’s own ordering is by identifier.
How to use it
A realistic cross-border check — one company, two jurisdictions, officers and LEI on:
{
"query": "Equinor",
"countries": ["NO", "GB"],
"maxRecordsPerCountry": 5
}
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("shelvick/global-company-registry-search").call(
run_input={"query": "Equinor", "countries": ["NO", "GB"], "maxRecordsPerCountry": 5}
)
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
if row["outcome"] == "match":
print(row["country"], row["legal_name"], row["registration_number"], row["lei"])
print(" source:", row["source_url"])
else:
print(row["country"], row["outcome"], "→", row["message"])
The Norwegian record that comes back:
{
"outcome": "match",
"query": "Equinor",
"country": "NO",
"registry": "Brønnøysundregistrene — Enhetsregisteret (Norway)",
"legal_name": "EQUINOR ASA",
"registration_number": "923609016",
"status": "Registrert",
"status_normalized": "active",
"legal_form": "Allmennaksjeselskap",
"incorporation_date": "1972-09-18",
"registered_address": "Forusbeen 50, 4035, STAVANGER, Norge",
"activity_codes": [
{ "scheme": "NACE", "code": "06.100", "label": "Utvinning av råolje" }
],
"officers": [
{ "name": "Anders Opedal", "role": "Daglig leder" },
{ "name": "Jarle Kjell Roth", "role": "Styrets leder" }
],
"lei": "OW6OFBNCKXC4US5C7523",
"source_url": "https://virksomhet.brreg.no/nb/oppslag/enheter/923609016",
"retrieved_at": "2026-09-18T09:41:02Z",
"match_type": "name"
}
Omit countries entirely to sweep every country in the coverage list; outcome tells you which ones delivered and which explained themselves. Each run also writes two key-value-store records: OUTPUT with the per-outcome counts, and COVERAGE with the live coverage manifest — which countries this build searches, which publish officers, and each register’s caveat — so an integration can read current coverage rather than trust a document that may have aged.
If you’re calling from an MCP-enabled agent, the Actor is exposed as a tool through the Apify MCP server and the input schema’s per-field descriptions are advertised to the model, so an agent can construct a correct call without this page. The outcome plus message contract is written for exactly that reader: it lets an agent distinguish “this company does not exist in Finland” from “Finland could not be reached,” which is the distinction most company-lookup tools destroy by returning an empty array for both.
How it compares to the alternatives
| Approach | Countries per call | Reads the official register | One schema across countries | Citation per record | Key or account |
|---|---|---|---|---|---|
| Single-country registry scraper | one | yes | n/a | sometimes | sometimes |
| Thin wrapper over a few registry APIs | two to four | yes | partly | rarely | sometimes |
| Commercial company-data aggregator | many | no — a licensed copy | yes | no | yes, paid |
| This Actor | up to eleven, plus LEI | yes | yes | yes, every record | no |
Each of those three properties exists somewhere on its own. Breadth exists, at aggregators, without citations. Primary-source fidelity exists, in single-country tools, without breadth. Normalization exists in thin multi-API wrappers, over two to four countries. The bet here is that the intersection — breadth, under one schema, over primary sources, with a citation on every record and no key to provision — is the thing that is actually missing, and that a cross-border verification workflow values it more than any one of the three alone.
The comparison has an honest reverse direction. If you need one country and need it deeply — full filing history, financial statements, document images — a dedicated single-country tool built on that register’s richer surfaces will beat this. This Actor is optimized for the breadth-plus-identity question, not for depth in any one jurisdiction.
Pricing model
Pay-per-event, and the event is a delivered record: one charge per matched entity record, pushed to the dataset before it is charged. Rows reporting no match, no coverage, a source failure, a rate limit or an unusable request are never charged, so an all-countries query that finds the company in two of them costs two records rather than eleven lookups. A platform Actor-start event is amortized across the whole query, and maxRecordsPerCountry bounds what a single broad name search can bill.
Charging on delivery rather than on attempt is the only structure that survives this Actor’s failure model. A register being down, rate-limiting, or simply not containing the company are all normal outcomes of a legitimate query, and all three are things the caller cannot control. Billing them would make the honest per-country rows — the ones that make the output legible — into a cost, which is exactly backwards.
Current per-event rates and any subscriber discounts are on the Apify Store Pricing tab, which is authoritative.
Open questions / future work
- Lithuania needs a decision, not a retry. Either an official surface that answers without a browser, or accepting a browser tier for one country and the cost profile that comes with it. Its hundred-searches-per-day cap means it will be a low-volume source whenever it lands.
- Polish officers wait on a terms ruling. The register does publish board representation; its dataset reuse terms don’t yet clearly permit systematic reuse. That is a reading exercise, not an engineering one, and it is the sequence I intend to keep.
- The next registers are each gated on something specific, which is why they aren’t here yet: some need a free key I have to request, some need credentials, one needs an application with a multi-week turnaround, and a few sit behind reuse terms that forbid systematic retrieval outright. That last group is not a queue — those countries are declined, not pending.
- Officers pagination in the UK. Fetching beyond the first officers page is straightforward and adds requests per entity; whether that is worth the added latency on a broad name search is an open tradeoff rather than a decided one.
- Relevance ranking over registers that have none. Today the answer is “raise the per-country cap.” A defensible alternative would be to rank only within what the register returned, and to label it clearly as ours rather than the source’s. I’d want the labelling settled before the ranking.