Company register number: look it up in the country's register, through GLEIF, or link to the register #935

Closed
opened 2026-10-16 13:27:47 +00:00 by tiagoagueda · 2 comments
Owner

The Company register number identifier is only checked for its shape. Many countries now offer a free online lookup of their company register, so Postulo could confirm the number, fill in the company's details from it, and link to the register's own page.

What exists today

  • The scheme. src/postulo/plugins/identifiers/schemes.py declares Scheme(REGISTER, _("Company register number"), …):
    • subjects=ONLY_COMPANY;
    • the value is a two-letter country, a space, then the number as the register writes it (PT 501234567, FR 552081317, DE HRB 12345);
    • the only check is starts_with_a_country.
  • No link and no lookup. Unlike LinkedIn, Wikidata or OpenCorporates, the scheme has no link, and nothing is looked up.
  • Precedents. Company data is already fetched on demand from outside sources: Wikidata (#673, names and place) and the logo finder.

What is available (researched 2026-10-16)

The EU rules

Commission Implementing Regulation (EU) 2023/138 (high-value datasets) has applied since 2024. It lists companies and company ownership and requires registers to make basic company data free, machine-readable and available through an API (bulk too where relevant), under CC BY 4.0 or less restrictive terms (data.europa.eu). Registers are still opening up under it: the Netherlands and Sweden recently, others to follow.

Free official APIs per country

Country Service Key Notes
FR API Recherche d'Entreprises (SIRENE + RNE) none by SIREN/SIRET or name; about 7 req/s (source)
GB Companies House API free key 600 req / 5 min; monthly bulk
NO Brønnøysund Enhetsregisteret (data.brreg.no) none basic data free; nightly bulk
FI PRH / YTJ open data (avoindata.prh.fi) none API version to confirm
CZ ARES none REST by IČO
EE Äriregister none REST + open bulk data
PL KRS (api-krs.ms.gov.pl) none by KRS number only (no name or NIP search)
NL KVK open dataset API key BV/NV only, reduced fields, 100 req / 5 min
DK CVR approval free once access is granted
SE Bolagsverket key free basic API since 2025 (high-value datasets)
BE KBO/BCE — daily open-data CSV; the search webservice is paid

No official API (a web search only): DE (handelsregister.de: per-company retrieval, no bulk), PT (Justiça portal search; the full certificate needs the company's access code), ES, IT, CH (Zefix: free search, extracts are cantonal and paid).

Across countries

  • GLEIF (LEI) API. Free, no key, CC0. An LEI record carries the local register number (registeredAs) and which register issued it (registeredAt). So for any company with an LEI, which Postulo already stores as an identifier, a single call gives its register number in any country.
  • EU "Find a company" (BRIS) on the e-Justice portal. It covers every EU register plus IS, LI and NO, and is free, but has no public API. It suits a link-out.
  • OpenCorporates. Needs a key; it is free only for public-benefit users (terms). Not suitable as a default.
  • A maintained reference of endpoints, auth and limits per country: awesome-business-registries.

What would change

  1. A register lookup, per country, run on request.
    • When the person presses Look up beside a register number, Postulo asks that country's register.
    • It confirms the number exists, and offers the official name, address and status, and the activity code (NACE, see #897) where given. The person chooses what to keep; nothing is overwritten silently.
    • Requests go through the guarded HTTP client with Postulo's User-Agent and are rate-limited.
    • Only company data is sent, never personal data.
  2. Adapters, keyless first.
    • One small adapter per country, behind one interface (a plugin kind or a built-in registry like link_services, decided when implementing).
    • Start with FR, NO, FI, CZ, EE and PL.
    • GB, NL, DK and SE work once the operator enters a key in Server settings.
  3. GLEIF.
    • When a company has an LEI and no register number, offer the register number from the LEI record.
    • When it has both, check that they agree.
  4. A link for every country.
    • Give the scheme a link per country to the register's own search page.
    • Where a register has no deep link, use the EU BRIS search.
  5. A check per country, where a rule is known. For example, a French SIREN's Luhn digit, or a Czech IČO checksum.
  6. Paperwork.
    • The record of processing and data-protection page list each outside service the instance may call.
    • THREAT-MODEL notes the new outbound requests.
    • Attribution follows each source's licence (CC BY 4.0 where required).
    • New text goes through the catalogues (Weblate).

To confirm before implementing

These came from secondary sources and need checking against each register's own pages:

  • Germany's "free since 2022";
  • PRH's current API version;
  • Zefix's endpoints;
  • CVR's access terms;
  • OpenCorporates' free-tier limits;
  • Portugal's lack of an API.

#673 (Wikidata lookup), #897 (NACE sector), #913 (outbound geocoding: same rate-limit and privacy pattern).

The *Company register number* identifier is only checked for its shape. Many countries now offer a free online lookup of their company register, so Postulo could confirm the number, fill in the company's details from it, and link to the register's own page. ## What exists today - **The scheme.** `src/postulo/plugins/identifiers/schemes.py` declares `Scheme(REGISTER, _("Company register number"), …)`: - `subjects=ONLY_COMPANY`; - the value is a two-letter country, a space, then the number as the register writes it (`PT 501234567`, `FR 552081317`, `DE HRB 12345`); - the only check is `starts_with_a_country`. - **No link and no lookup.** Unlike LinkedIn, Wikidata or OpenCorporates, the scheme has no `link`, and nothing is looked up. - **Precedents.** Company data is already fetched on demand from outside sources: Wikidata (#673, names and place) and the logo finder. ## What is available (researched 2026-10-16) ### The EU rules Commission Implementing Regulation (EU) 2023/138 (high-value datasets) has applied since 2024. It lists *companies and company ownership* and requires registers to make basic company data **free, machine-readable and available through an API** (bulk too where relevant), under CC BY 4.0 or less restrictive terms ([data.europa.eu](https://data.europa.eu/en/news-events/news/high-value-datasets-what-has-changed-and-what-will-come-next)). Registers are still opening up under it: the Netherlands and Sweden recently, others to follow. ### Free official APIs per country | Country | Service | Key | Notes | |---|---|---|---| | FR | [API Recherche d'Entreprises](https://www.data.gouv.fr/dataservices/api-recherche-dentreprises/discussions) (SIRENE + RNE) | none | by SIREN/SIRET or name; about 7 req/s ([source](https://github.com/annuaire-entreprises-data-gouv-fr/search-api)) | | GB | Companies House API | free key | 600 req / 5 min; monthly bulk | | NO | Brønnøysund Enhetsregisteret (data.brreg.no) | none | [basic data free](https://www.brreg.no/en/use-of-data-from-the-bronnoysund-register-centre/); nightly bulk | | FI | PRH / YTJ open data (avoindata.prh.fi) | none | API version to confirm | | CZ | [ARES](https://otwarteapi.pl/en/ares) | none | REST by IČO | | EE | [Äriregister](https://otwarteapi.pl/en/rik-ariregister) | none | REST + open bulk data | | PL | KRS (api-krs.ms.gov.pl) | none | by KRS number only (no name or NIP search) | | NL | [KVK open dataset API](https://developers.kvk.nl/documentation/open-dataset-basis-bedrijfsgegevens-api) | key | BV/NV only, reduced fields, 100 req / 5 min | | DK | CVR | approval | free once access is granted | | SE | Bolagsverket | key | free basic API since 2025 (high-value datasets) | | BE | KBO/BCE | — | daily open-data CSV; the search webservice is paid | **No official API (a web search only):** DE (handelsregister.de: per-company retrieval, no bulk), PT (Justiça portal search; the full certificate needs the company's access code), ES, IT, CH (Zefix: free search, extracts are cantonal and paid). ### Across countries - **GLEIF (LEI) API.** Free, no key, CC0. An LEI record carries the local register number (`registeredAs`) and which register issued it (`registeredAt`). So for any company with an LEI, which Postulo already stores as an identifier, a single call gives its register number in any country. - **EU "Find a company" (BRIS)** on the [e-Justice portal](https://e-justice.europa.eu/topics/registers-business-insolvency-land/business-registers-search-company-eu/general-information-find-company_en). It covers every EU register plus IS, LI and NO, and is free, but has no public API. It suits a link-out. - **OpenCorporates.** Needs a key; it is free only for public-benefit users ([terms](https://opencorporates.com/terms-of-use-2/)). Not suitable as a default. - **A maintained reference** of endpoints, auth and limits per country: [awesome-business-registries](https://github.com/jkbngb/awesome-business-registries). ## What would change 1. **A register lookup, per country, run on request.** - When the person presses *Look up* beside a register number, Postulo asks that country's register. - It confirms the number exists, and offers the official name, address and status, and the activity code (NACE, see #897) where given. The person chooses what to keep; nothing is overwritten silently. - Requests go through the guarded HTTP client with Postulo's User-Agent and are rate-limited. - Only company data is sent, never personal data. 2. **Adapters, keyless first.** - One small adapter per country, behind one interface (a plugin kind or a built-in registry like `link_services`, decided when implementing). - Start with FR, NO, FI, CZ, EE and PL. - GB, NL, DK and SE work once the operator enters a key in *Server settings*. 3. **GLEIF.** - When a company has an LEI and no register number, offer the register number from the LEI record. - When it has both, check that they agree. 4. **A link for every country.** - Give the scheme a `link` per country to the register's own search page. - Where a register has no deep link, use the EU BRIS search. 5. **A check per country, where a rule is known.** For example, a French SIREN's Luhn digit, or a Czech IČO checksum. 6. **Paperwork.** - The record of processing and data-protection page list each outside service the instance may call. - THREAT-MODEL notes the new outbound requests. - Attribution follows each source's licence (CC BY 4.0 where required). - New text goes through the catalogues (Weblate). ## To confirm before implementing These came from secondary sources and need checking against each register's own pages: - Germany's "free since 2022"; - PRH's current API version; - Zefix's endpoints; - CVR's access terms; - OpenCorporates' free-tier limits; - Portugal's lack of an API. ## Related #673 (Wikidata lookup), #897 (NACE sector), #913 (outbound geocoding: same rate-limit and privacy pattern).
Author
Owner

How to build it: inside the identifier module, or as a module beside it

What the code already says

IdentifierPlugin in plugins/base.py sets two rules that decide most of this:

  • A scheme "must not reach the network". The contract says: "A scheme validates what somebody typed and knows where it links; asking somebody else whether the identifier exists is a deliberate act for another day, and a plugin is not the loophole for it." crosschecks.py (#675) says the same: comparing a company's identifiers against an outside record "is a lookup, which is a deliberate act".
  • An identifier plugin cannot be switched off. Switching it off would leave every stored identifier unreadable.

A register lookup is a network call that an operator, or a person, must be able to switch off. So the two rules point the same way.

Option A: build it into the identifier module

Give Scheme an optional lookup(value) -> Record | None and put the per-country adapters in plugins/identifiers/.

  • For: one place. The lookup sits next to the scheme it serves, and there is no new module.
  • Against:
    • It breaks the identifier contract's no-network rule. That contract is what lets identifiers be an always-on plugin with nothing to consent to.
    • There is nothing to switch off. The lookup would need its own on/off and permission settings tucked inside a module that has none.
    • Every country adapter, its keys and its terms of use would live in the identifier package. A third party couldn't add a country without patching it.
    • The record of processing would have to describe the identifier module as one that calls outside services.

A feature plugin, Register lookup (name to be decided), that depends on identifiers and adds a lookup to the schemes that have an adapter.

  • The identifier module stays offline and always on, as its contract says. The new plugin only reads a scheme's value and offers a Look up action beside it.
  • Switchable like any feature. It is off by default, or offered per instance. Server settings → Plugins decides who may use it, and holds the API keys (GB, NL, DK, SE) and the outbound rate limit.
  • Adapters register through an open entry-point group, as link services do (postulo.link_services, #305). That group is open because "a registry somebody else wrote can add to what is offered and can refuse nothing". The same holds here:
    • Postulo ships the keyless adapters (FR, NO, FI, CZ, EE, PL) and GLEIF;
    • a country plugin can add another without touching core.
  • One place for the outbound rules. The guarded HTTP client, the User-Agent, rate limits, caching, and the record-of-processing and THREAT-MODEL entries all sit in one place. The geocoder in #913 is the same pattern, so the two could share that groundwork.
  • The offline parts stay in the identifier module. A scheme's per-country link to the register's search page, and per-country checksums such as the SIREN Luhn digit or the IČO check, belong to the scheme and are added there.

Recommendation

Option B. Split the work along the contract's own line:

  • In identifiers, offline:
    • a per-country link to each register's search page, with the EU BRIS search as the fallback;
    • per-country checksums;
    • an offline cross-check between a register number and the country in an LEI record, where one is stored.
  • In a new switchable plugin, online:
    • the Look up action;
    • the per-country adapters behind an open entry-point group;
    • GLEIF's registeredAs / registeredAt;
    • keys, rate limits and the data-protection entries.

The offline half can land first. It already gives every register number a working link, and needs no decision about outbound requests.

## How to build it: inside the identifier module, or as a module beside it ### What the code already says `IdentifierPlugin` in `plugins/base.py` sets two rules that decide most of this: - **A scheme "must not reach the network".** The contract says: *"A scheme validates what somebody typed and knows where it links; asking somebody else whether the identifier exists is a deliberate act for another day, and a plugin is not the loophole for it."* `crosschecks.py` (#675) says the same: comparing a company's identifiers against an outside record "is a lookup, which is a deliberate act". - **An identifier plugin cannot be switched off.** Switching it off would leave every stored identifier unreadable. A register lookup is a network call that an operator, or a person, must be able to switch off. So the two rules point the same way. ### Option A: build it into the identifier module Give `Scheme` an optional `lookup(value) -> Record | None` and put the per-country adapters in `plugins/identifiers/`. - **For:** one place. The lookup sits next to the scheme it serves, and there is no new module. - **Against:** - It breaks the identifier contract's no-network rule. That contract is what lets identifiers be an always-on plugin with nothing to consent to. - There is nothing to switch off. The lookup would need its own on/off and permission settings tucked inside a module that has none. - Every country adapter, its keys and its terms of use would live in the identifier package. A third party couldn't add a country without patching it. - The record of processing would have to describe the identifier module as one that calls outside services. ### Option B: a separate module beside it (recommended) A feature plugin, *Register lookup* (name to be decided), that depends on `identifiers` and adds a lookup to the schemes that have an adapter. - **The identifier module stays offline and always on**, as its contract says. The new plugin only *reads* a scheme's value and offers a *Look up* action beside it. - **Switchable like any feature.** It is off by default, or offered per instance. *Server settings → Plugins* decides who may use it, and holds the API keys (GB, NL, DK, SE) and the outbound rate limit. - **Adapters register through an open entry-point group**, as link services do (`postulo.link_services`, #305). That group is open because "a registry somebody else wrote can add to what is offered and can refuse nothing". The same holds here: - Postulo ships the keyless adapters (FR, NO, FI, CZ, EE, PL) and GLEIF; - a country plugin can add another without touching core. - **One place for the outbound rules.** The guarded HTTP client, the User-Agent, rate limits, caching, and the record-of-processing and THREAT-MODEL entries all sit in one place. The geocoder in #913 is the same pattern, so the two could share that groundwork. - **The offline parts stay in the identifier module.** A scheme's per-country `link` to the register's search page, and per-country checksums such as the SIREN Luhn digit or the IČO check, belong to the scheme and are added there. ### Recommendation **Option B.** Split the work along the contract's own line: - **In `identifiers`, offline:** - a per-country link to each register's search page, with the EU BRIS search as the fallback; - per-country checksums; - an offline cross-check between a register number and the country in an LEI record, where one is stored. - **In a new switchable plugin, online:** - the *Look up* action; - the per-country adapters behind an open entry-point group; - GLEIF's `registeredAs` / `registeredAt`; - keys, rate limits and the data-protection entries. The offline half can land first. It already gives every register number a working link, and needs no decision about outbound requests.
Author
Owner

The offline half is in PR #1022 (per-country check digits and register links, EU business registers search as the fallback). The online lookup is tracked in #1021; an LEI cross-check needs the GLEIF record and moved there. This issue stays open for it.

The offline half is in PR #1022 (per-country check digits and register links, EU business registers search as the fallback). The online lookup is tracked in #1021; an LEI cross-check needs the GLEIF record and moved there. This issue stays open for it.
tiagoagueda added this to the 0.5.0 milestone 2026-10-25 17:04:12 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#935
No description provided.