# registry-mcp — the company registry MCP: full reference for LLM readers

Company data for AI agents, any country. One shape, many national registries.
This document is written for a model deciding which call to make. Every section
says what the call is for, when to use it, what comes back, and what to do on
each error.

Base URL: https://api.foretak.dev
MCP endpoint (Streamable HTTP): https://api.foretak.dev/mcp
Local stdio: `uvx registry-mcp` or `npx registry-mcp`
Source (MIT): https://github.com/foretak/registry-mcp
Short version of this file: https://api.foretak.dev/llms.txt

Aliases you may be searching for: brreg, Brønnøysund, Brønnøysundregistrene,
Enhetsregisteret, organisasjonsnummer, orgnr, org.nr, Norway company lookup,
Norwegian business registry, foretak, Companies House, Companies House API,
company number, company registration number, CRN, UK company lookup, UK company
search, confirmation statement, Bolagsverket, organisationsnummer, svenskt
företag, företagsinformation, kontrollera företag, årsredovisning, F-skatt,
Swedish company lookup, Swedish company number, company registry,
company registry MCP, MCP.


## 0. The one thing to know first

REST and MCP return the **same JSON documents**. A `CompanyReport` from
`GET /v1/NO/company/923609016` is byte-identical to the one from the MCP tool
`lookup_company("923609016", country="NO")`. Learn the shape once.

Unknown is always `null` — never `""`, never `0`. `"employees": null` means the
register does not publish a figure; `"employees": 0` means it publishes zero.
`employees_reported` tells the two apart explicitly.

Dates are ISO-8601 `YYYY-MM-DD`. Timestamps are timezone-aware UTC.
Country codes are ISO-3166-1 alpha-2, upper-case.


## 1. Countries currently supported

| Country | Code | Register | Identifier | Example |
|---|---|---|---|---|
| Norway | `NO` | Enhetsregisteret (Brønnøysundregistrene), slug `brreg` | organisasjonsnummer (orgnr) | `923609016` |
| United Kingdom | `GB` | Companies House, slug `companies-house` | company number (company registration number, CRN) | `00445790` |
| Sweden | `SE` | Bolagsverket (with Statistics Sweden/SCB in the same payload), slug `bolagsverket` | organisationsnummer — ten digits; a sole trader's is their twelve-digit personnummer | `5560160680` |

Do not hard-code this table — call `list_countries()` / `GET /v1/countries`.
Countries are added as separate modules; the list grows without the tool shape
changing.

**The United Kingdom is `GB`, never `UK`.** Country codes are ISO-3166-1
alpha-2 with no alias table, so `GB` is the only code that routes; `UK` returns
`404 unsupported_country` with a hint naming the supported codes. Write "UK" to
a user, send `GB` to this API.

**Companies House needs a credential.** `GB` is the first module whose upstream
requires an API key, so every `CountryInfo` row carries `requires_api_key` and
`api_key_env`. On the hosted service at api.foretak.dev the key is configured
and `GB` answers. On a self-hosted deployment with no
`COMPANIES_HOUSE_API_KEY`, `GB` calls return `502 upstream_error` naming that
variable while every other country keeps answering — the flag tells you a key
is *needed*, not whether this deployment *has* one, so read the error when one
arrives rather than pre-judging from the flag.

**Bolagsverket needs a credential too**, an OAuth 2 client pair rather than a
key: `BOLAGSVERKET_CLIENT_ID` and `BOLAGSVERKET_CLIENT_SECRET`, issued on
request. `requires_api_key` is `true` for `SE` and `api_key_env` names the
first of the pair. The hosted service has them configured and `SE` answers.

**Sweden has no name search, permanently.** Bolagsverket's free "värdefulla
datamängder" API has four operations and none of them takes a company name, so
`search_company` for `SE` returns `501 not_implemented` with a hint naming the
alternatives. This is a fact about the register, not a gap in this module: look
a Swedish company up by its organisationsnummer, or use Bolagsverket's bulk
downloadable files if you genuinely need a name index.

**Sweden publishes no status field.** Norway publishes four lifecycle flags and
this service derives `active` from the absence of all four; the UK publishes one
status string and it is mapped one-to-one; Sweden publishes neither, and
`status` is derived from three independent signals — a strike-off date, any
ongoing winding-up or restructuring procedure, and Statistics Sweden's
"economically active" marking. The consequence is worth carrying into your
answer: **for `SE`, `is_active: true` means "on the register and not winding
down", which is not the same as trading.** A dormant but perfectly registered
Swedish company is `active` with a `notes` entry saying Statistics Sweden does
not mark it as economically active. Read `status_detail` and `notes`, not just
the boolean.


## 2. Choosing a call

- You have a national identifier and want facts  → `lookup_company`
- You have only a company name                    → `search_company`, then
  `lookup_company` with the `id` of the right hit. Not available for `SE`:
  Bolagsverket's free API has no name index, so `SE` needs an identifier
- You want to know if an identifier is well-formed, with no network round-trip
  → `validate_company_id`
- You want filing dates for a company             → `company_deadlines`
- You want to know which countries work           → `list_countries`

Never guess an identifier. If `search_company` returns several hits, show the
user the candidates with their `confidence` and `confidence_basis` rather than
picking one silently.

Pick the country code before the call, not after: `NO` for a Norwegian
organisasjonsnummer (nine digits), `GB` for a UK company number (eight
characters, digits or two letters and six digits), `SE` for a Swedish
organisationsnummer (ten digits, written `556016-0680`). A user saying "this UK
supplier" means `country="GB"`.

Nine digits, ten digits, eight characters — the lengths are the fastest
discriminator when a user pastes a bare number with no country. But do not rely
on length alone across the two Nordic spellings: Norwegian
*organisasjonsnummer* and Swedish *organisationsnummer* differ by one letter and
mean different registers. If the user's text is ambiguous, ask, or call
`validate_company_id` in each candidate country first — it costs no network
round trip.


## 3. REST endpoints

### 3.1 GET /v1/countries

**For:** discovering which national registries this service can answer for, and
what each one's identifier looks like.
**When:** before your first call in a new country, or when a user names a
country you have not used here before.

    curl https://api.foretak.dev/v1/countries

Response — a `CountriesResponse` document, one `CountryInfo` row per registry
that can answer right now, sorted by country code. This is the complete
response today, not an excerpt:

    {
      "countries": [
        {
          "country": "GB",
          "registry": "companies-house",
          "name": "Companies House (United Kingdom)",
          "id_scheme": "company number",
          "id_example": "00445790",
          "id_description": "A UK company registration number (CRN): 8 characters, either 8 digits or a two-letter prefix and 6 digits. Shorter numbers are zero-padded, so 445790 is written 00445790. There is no check digit.",
          "source_url": "https://api.company-information.service.gov.uk",
          "license": "Crown copyright — Companies House public register, free to re-use",
          "is_stub": false,
          "requires_api_key": true,
          "api_key_env": "COMPANIES_HOUSE_API_KEY"
        },
        {
          "country": "NO",
          "registry": "brreg",
          "name": "Enhetsregisteret (Brønnøysundregistrene)",
          "id_scheme": "organisasjonsnummer",
          "id_example": "923609016",
          "id_description": "A Norwegian organisasjonsnummer (orgnr): nine digits, the ninth a MOD11 check digit. Written '923 609 016' or '923609016'; a VAT number adds 'MVA'.",
          "source_url": "https://data.brreg.no/enhetsregisteret/api",
          "license": "NLOD 2.0",
          "is_stub": false,
          "requires_api_key": false,
          "api_key_env": null
        },
        {
          "country": "SE",
          "registry": "bolagsverket",
          "name": "Bolagsverket (Sweden)",
          "id_scheme": "organisationsnummer",
          "id_example": "5560160680",
          "id_description": "A Swedish organisationsnummer: ten digits, written 556016-0680, with a check digit. A sole trader is looked up by a twelve-digit personnummer instead (YYYYMMDDNNNN), and one such number can carry several registered businesses.",
          "source_url": "https://gw.api.bolagsverket.se/vardefulla-datamangder/v1",
          "license": "Free re-use (Bolagsverket/SCB high-value datasets, EU Open Data Directive) — the publisher names no licence",
          "is_stub": false,
          "requires_api_key": true,
          "api_key_env": "BOLAGSVERKET_CLIENT_ID"
        }
      ]
    }

`id_example` is a real, valid identifier — use it to smoke-test the service in
a country you have not called before, rather than guessing one. `is_stub` marks
example/template modules, which are hidden from this list unless stubs are
explicitly requested; a row you can see is a registry that answers.

`requires_api_key` says the *upstream register* needs a credential, and
`api_key_env` names the environment variable that carries it (`null` when none
is needed). It is a declaration about the register, never a health check on
this deployment: a row with `requires_api_key: true` is still listed whether or
not the key is set here. If a `GB` call then fails with `502 upstream_error`,
its `hint` names `COMPANIES_HOUSE_API_KEY` and where to get a free one.

MCP `list_countries` returns this identical document, both keys included.

Errors: none expected. A 500 means a bug here — retry once, then tell the user.

### 3.2 GET /v1/{country}/company/{id}

**For:** the full report on one registered entity.
**When:** you already have the national identifier — from the user, an invoice,
a contract, or a previous `search_company` hit.

    curl https://api.foretak.dev/v1/NO/company/923609016

Response (abridged — every field is documented in §5):

    {
      "country": "NO",
      "registry": "brreg",
      "id": "923609016",
      "id_formatted": "923 609 016",
      "id_scheme": "organisasjonsnummer",
      "euid": null,
      "name": "EQUINOR ASA",
      "previous_names": ["STATOIL ASA", "STATOILHYDRO ASA", "STATOIL ASA",
                         "Den norske stats oljeselskap a.s"],
      "legal_form_code": "ASA",
      "legal_form": "Public limited company",
      "legal_form_local": "Allmennaksjeselskap",
      "limited_liability": true,
      "has_board_duty": true,
      "has_annual_accounts_duty": true,
      "status": "active",
      "status_detail": "Registered and active in Enhetsregisteret.",
      "is_active": true,
      "registered_at": "1995-03-12",
      "founded_at": "1972-09-18",
      "business_register_registered_at": "1988-04-28",
      "vat_registered": true,
      "vat_registered_at": "1989-07-01",
      "vat_number": "NO923609016MVA",
      "in_business_register": true,
      "registers": {
        "foretaksregisteret": true, "stiftelsesregisteret": false,
        "frivillighetsregisteret": false, "partiregisteret": false,
        "mvaregisteret": true
      },
      "employees": 21239,
      "employees_reported": true,
      "industry_codes": [
        {"code": "06.100", "description": "Utvinning av råolje",
         "scheme": "NACE", "rank": 1},
        {"code": "06.200", "description": "Utvinning av naturgass",
         "scheme": "NACE", "rank": 2},
        {"code": "19.200", "scheme": "NACE", "rank": 3,
         "description": "Produksjon av raffinerte petroleumsprodukter og fossile brenselsprodukter"}
      ],
      "sector_code": "1120",
      "sector": "Statlig eide aksjeselskaper mv.",
      "share_capital": 5976872600.0,
      "share_capital_currency": "NOK",
      "business_address": {
        "lines": ["Forusbeen 50"], "postal_code": "4035",
        "city": "STAVANGER", "municipality": "STAVANGER",
        "municipality_code": "1103", "country_code": "NO",
        "country_name": "Norge"
      },
      "website": "www.equinor.com",
      "email": null,
      "phone": "51 99 00 00",
      "advertising_protected": null,
      "parent_id": null,
      "is_subunit": false,
      "in_group": true,
      "last_annual_accounts_year": 2025,
      "confidence": 1.0,
      "confidence_basis": "exact identifier lookup in Enhetsregisteret",
      "cached": false,
      "fetched_at": "2026-09-03T09:12:44Z",
      "source": "Enhetsregisteret (Brønnøysundregistrene)",
      "source_url": "https://data.brreg.no/enhetsregisteret/api/enheter/923609016",
      "license": "NLOD 2.0",
      "notes": []
    }

Identifier input is normalised for you: `923 609 016`, `923.609.016`,
`NO923609016MVA` and `NO 923 609 016 MVA` all resolve to `923609016`.

The same call for the United Kingdom, and the same document shape:

    curl https://api.foretak.dev/v1/GB/company/00445790

Response (abridged the same way; MCP
`lookup_company("00445790", country="GB")` returns this identical document):

    {
      "country": "GB",
      "registry": "companies-house",
      "id": "00445790",
      "id_formatted": null,
      "id_scheme": "company number",
      "euid": null,
      "name": "TESCO PLC",
      "previous_names": ["TESCO STORES (HOLDINGS) PUBLIC LIMITED COMPANY",
                         "TESCO STORES (HOLDINGS) LIMITED"],
      "legal_form_code": "plc",
      "legal_form": "Public limited company",
      "legal_form_local": "Public limited company",
      "limited_liability": true,
      "has_board_duty": true,
      "has_annual_accounts_duty": true,
      "status": "active",
      "status_detail": "Active on the Companies House register.",
      "is_active": true,
      "registered_at": "1947-11-27",
      "founded_at": "1947-11-27",
      "vat_registered": null,
      "vat_number": null,
      "registers": {"insolvency": false},
      "employees": null,
      "employees_reported": false,
      "industry_codes": [
        {"code": "47110", "description": null, "scheme": "SIC 2007", "rank": 1}
      ],
      "share_capital": null,
      "business_address": {
        "lines": ["Tesco House, Shire Park", "Kestrel Way"],
        "postal_code": "AL7 1GA", "city": "Welwyn Garden City",
        "municipality": null, "municipality_code": null,
        "country_code": "GB", "country_name": "United Kingdom"
      },
      "advertising_protected": null,
      "last_annual_accounts_year": 2026,
      "published_deadlines": [
        {"kind": "annual_accounts", "due_date": "2027-08-26",
         "period_start": "2026-03-01", "period_end": "2027-02-26",
         "overdue": false, "source": "accounts.next_accounts.due_on"},
        {"kind": "confirmation_statement", "due_date": "2027-07-02",
         "period_start": null, "period_end": "2027-06-18",
         "overdue": false, "source": "confirmation_statement.next_due"}
      ],
      "confidence": 1.0,
      "confidence_basis": "exact identifier lookup in the Companies House register",
      "cached": false,
      "fetched_at": "2026-09-04T16:56:06Z",
      "source": "Companies House (UK)",
      "source_url": "https://find-and-update.company-information.service.gov.uk/company/00445790",
      "license": "Crown copyright — Companies House public register, free to re-use",
      "notes": []
    }

UK identifier input is normalised too, but differently: a short number is
zero-padded and a letter prefix upper-cased, so `445790` → `00445790` and
`oc303675` → `OC303675`. `id_formatted` is `null` because UK company numbers
have no conventional grouping — that is honesty, not a missing feature.

**What Companies House does not publish, and therefore returns as `null`:**
`vat_registered`, `vat_registered_at` and `vat_number` (UK VAT is HMRC's
separate register, and a VAT number is not derivable from a company number);
`employees` — the UK register carries no employee count at all, for any
company, so `employees_reported` is always `false` for `GB`; `share_capital`
(it lives in the confirmation statement, not the profile); `purpose`,
`website`, `email`, `phone`, `sector`. Norway publishes most of these, the UK
publishes none of them: same shape, different coverage. Do not read a `null`
here as "this company has no employees" or "not VAT-registered" — read it as
"this register does not say".

`industry_codes` for `GB` are SIC 2007 codes with `description: null`;
Companies House returns the code only. `registers` carries one key for
Britain, `insolvency`, from the register's own `has_insolvency_history`.
**`charges` is deliberately absent.** Companies House
publishes `has_charges: false` for companies that do have charges — TESCO PLC says
false while the charges endpoint returns nine, two still outstanding (recorded live
2026-09-08). A key here asserts membership of a sub-register; we can assert neither,
so we say nothing rather than say no. Ask `include=["charges"]` for the real answer:
it calls the endpoint and returns a present, empty block for a company with none.

Errors:
- `400 invalid_id` — the identifier fails the country's checksum or format.
  Do **not** retry the same string. Either fix the digits or call
  `search_company` with the name.
- `404 not_found` — well-formed but no such entity. The number may never have
  been issued, or the entity was deleted. Call `search_company` with the name.
- `404 unsupported_country` — no module for that country code. Call
  `list_countries` and pick a supported one; do not invent a route.
- `502 upstream_error` / `504 upstream_timeout` — the national register is
  down or slow. We already retried once. Wait ~60 s and retry at most once,
  then tell the user the register is unavailable. Do not loop.
- `429 rate_limited` — you exceeded 60 requests/minute per IP. Back off for the
  number of seconds in `details.retry_after` and batch your calls.

Two of these read differently for `GB`. A `404 not_found` hint adds that **sole
traders and ordinary partnerships are not registered at Companies House at
all** — a UK business can be entirely real and never appear here, which is not
true of Norway, where a sole proprietorship (ENK) has an organisasjonsnummer.
And `502 upstream_error` on `GB` has a second cause besides an upstream
outage: no Companies House API key configured on this deployment. Its `message`
says so and its `hint` names `COMPANIES_HOUSE_API_KEY`; that one is not fixed
by retrying, so tell the user rather than looping.

The same call for Sweden, and the same document shape again — this is the live
0.3.0 response, abridged to the fields that differ:

    curl https://api.foretak.dev/v1/SE/company/5560160680

    {
      "country": "SE",
      "registry": "bolagsverket",
      "id": "5560160680",
      "id_formatted": "556016-0680",
      "id_scheme": "organisationsnummer",
      "euid": null,
      "name": "Telefonaktiebolaget LM Ericsson",
      "legal_form_code": "AB",
      "legal_form": "Private or public limited company",
      "legal_form_local": "Aktiebolag",
      "limited_liability": true,
      "has_board_duty": true,
      "has_annual_accounts_duty": true,
      "status": "active",
      "status_detail": "Registered with Bolagsverket and not marked as struck off or in any winding-up or restructuring procedure.",
      "is_active": true,
      "registered_at": "1918-08-19",
      "vat_registered": null,
      "employees": null,
      "employees_reported": false,
      "industry_codes": [
        {"code": "70100", "description": "Verksamheter som utövas av huvudkontor", "scheme": "SNI 2007", "rank": 1}
      ],
      "postal_address": {"lines": [], "postal_code": "16483", "city": "STOCKHOLM", "country_code": "SE"},
      "advertising_protected": null,
      "published_deadlines": [],
      "confidence": 1.0,
      "confidence_basis": "exact identifier lookup in the Bolagsverket register",
      "source": "Bolagsverket (bolagsverket.se)",
      "source_url": "https://gw.api.bolagsverket.se/vardefulla-datamangder/v1",
      "license": "Free re-use (Bolagsverket/SCB high-value datasets, EU Open Data Directive) — the publisher names no licence",
      "notes": ["Filing deadlines are computed assuming a financial year ending 31 December. …"]
    }

Four things about `SE` specifically.

**`status` was derived, not read.** See §1: Bolagsverket publishes no status
field. `status_detail` is the sentence that says which of the three signals
decided it, and it is the field to quote. `is_active` for `SE` means *on the
register and not winding down* — a company can be `active` here and not be
trading, and when Statistics Sweden does not mark it economically active a
`notes` entry says exactly that, in English.

**`license` names no licence, deliberately.** Bolagsverket publishes this data
free under the EU high-value-datasets regime and names no licence for it, so
the string says what the permission is and says plainly that there is no licence
name to quote. A familiar name in that field would be a fabrication.

**What Bolagsverket's free dataset does not publish, and therefore returns as
`null`:** `vat_registered` and everything VAT (Skatteverket's register, not
Bolagsverket's); `employees`, so `employees_reported` is always `false` for
`SE`; `share_capital`; officers and beneficial owners; any financial figure;
the financial-year end; a visiting address, `email`, `phone` and `website`.
`business_address` is `null` and `postal_address` carries what there is.

**`published_deadlines` is `[]`.** Sweden publishes no per-company filing dates,
so both Swedish deadlines are computed from statute — see §3.4.

**`include=[...]` adds a second, independent fetch alongside the base report,
opt-in and at the cost of one extra upstream request per name.** Seven exist
today: `filings` answers *do they file, and on time* (`GB`, `NO` and `SE` all
declare it); `charges` answers *are assets already pledged* (`GB` only);
`insolvency` answers *are they being wound up* (`GB` only); `financials`
answers *what do the numbers say* (`NO` and `SE`); `lei` and `parents` answer
*who does GLEIF say this entity and its group are* (every country except
`SE`); and `peppol` answers *would an e-invoice reach them* (`NO` only). Ask
for more than one by repeating the parameter — REST
`?include=filings&include=charges`, MCP `include=["filings", "charges"]`.
Empty by default, which costs exactly one upstream request, same as before
this argument existed. Call `GET /v1/countries` (§3.1) / MCP
`list_countries` and read a country's `supported_includes` before guessing:
a name that country does not declare is a `400 bad_request` naming what it
does support, never a silently empty result.

**What a call costs you.** Measured 2026-09-09 with `count_tokens` on
`claude-opus-5` (other tokenizers differ by about 1%); approximate, and the
fixed figure below is the number
`tests/test_mcp.py::test_fixed_context_cost_stays_within_budget` pins.

- Loading this server: ~7,400 tokens once per conversation (7 tools +
  `instructions`), about a tenth of that on later turns once the client
  prompt-caches it.
- A base `lookup_company`: ~930 (`GB`) / ~1,430 (`NO`) / ~1,300 (`SE`).
- `+filings` ~3,500 for a 25-document page (~116 per document); `+charges`
  ~3,000 for 9 charges (~317 each — the register's own free text is the
  answer, D-045(a)); `+financials` ~900 (`NO`) / ~400 (`SE`).
- `+lei` ~220; `+parents` ~610; `+insolvency` ~220; every block at once,
  ~8,500.
- `company_deadlines` ~1,860 (`NO`); `list_countries` ~780;
  `validate_company_id` ~144 — which is why validating a spreadsheet column
  before looking a company up is 10× cheaper than looking it up.

**Two-level nullability, the same shape on every attachment.** The
attachment key on the response — `charges`, `filings`, `insolvency`,
`financials`, `lei`, `parents`, `peppol` — is `null` for one of two reasons:
you did not ask for it, or the fetch failed — the report's own top-level
`notes` names which attachment and why, in the second case. Once *present*,
an empty attachment (e.g. `"filings": {"documents": [], ...}`) is a real,
different answer: the register was asked and lists none for this entity.
Never read an absent attachment as "the register has none" — it may only
mean nobody asked.

**`filings` is one name for three differently-scoped answers**, because each
register publishes a different slice of an entity's filing life and none
publishes all of it: Companies House (`GB`) returns the whole filing
history, every filing kind, not only accounts; Bolagsverket (`SE`) returns
filed annual reports only; Regnskapsregisteret (`NO`) returns filed annual
accounts only. Read the scope for the country you asked, not the field name
— the same `filings` key means a different depth in each register.

**`insolvency` (`GB` only): read `is_liquidation` before concluding
distress.** `InsolvencyCase.is_liquidation: true` is not by itself evidence a
company cannot pay — a members' voluntary liquidation is a *solvent*
wind-up, begun by the directors' own declaration of solvency, and 56 of the
1,485 live British cases sampled were exactly that. A present block with
`cases: []` is also Companies House's ordinary answer for a solvent company,
byte-identical to its answer for a company number that was never issued — so
it is never evidence the entity exists, only that the register holds no case
against it.

**`financials` (`NO` and `SE`): the register's own key figures for the latest
filed accounting period, never a ratio or a verdict.** Norway and Sweden fill
this block from two different sources. Regnskapsregisteret's open dataset
publishes exactly one period — revenue (turnover), operating result, profit,
balance sheet totals, equity and liabilities — each figure beside its own
`currency`, because a figure separated from its currency is not partially
wrong, it is meaningless: 12 of 358 sampled Norwegian filings were not in
kroner. Sweden has no equivalent open key-figures feed, so this block is
read out of the entity's own filed annual report — the K2 inline-XBRL
document Bolagsverket's document API serves, the same `/dokumentlista` list
`filings` already reads, plus one further `/dokument` fetch for the most
recently filed report; `currency` there comes from the document's own unit,
never from a renamed, re-typed currency concept. Sweden's block is entity
accounts only, never a group, and a K3 (larger-company) filing carries a
caveat that no live K3 document had been read when this was built. Neither
register's figures are a ratio or a verdict: no equity ratio, current
ratio or similar field exists on either country's block, because Norway's
own totals do not always reconcile (27 of 358 sampled filings) and
Bolagsverket's own tagged solvency ratio was measured on the same company
at two different, undisambiguated scales — any ratio built from either
would be a verdict the register itself does not vouch for.
A `None` figure inside a present block means the company itself did not
report that line for this filing. An absent `financials` block means either
you did not ask, or the fetch failed, or — for Sweden — this company has
filed no digital annual report: Bolagsverket's channel holds nothing for it,
so the block is absent and the report's own top-level `notes` says so
(unlike `filings`, which returns a present block with `documents: []` for
the identical wire state). Norway returns a present block with
`periods: []` when Regnskapsregisteret holds no filed accounts. Britain does
not declare `financials` at all, so `include=["financials"]` for `GB` is a
`400 bad_request`, never an empty or absent block: the accounts of the companies that matter are filed
on paper or as PDF, the machine-readable (iXBRL) mandate is 1 April 2028
with a profit-and-loss publication opt-out for small and micro companies,
and no British filing sampled carried the balance-sheet totals this block
relays — a population fact about Companies House, not a parser this
project has declined to write.

**`lei` (every country except `SE`): the Legal Entity Identifier GLEIF, the
Global LEI Foundation, publishes for this entity.** CC0-licensed and
keyless. A present block with `"lei": null` means GLEIF holds no LEI for
this entity — a real, useful answer about a counterparty, not the same as
the block being absent. Sweden does not declare `lei`: the query sends the
identifier to a third-party host in a URL, and a Swedish identifier can be a
natural person's own (a sole trader's personnummer), so `include=["lei"]`
for `SE` is a `400 bad_request` naming Sweden's allowed set instead.

**`parents` (the same countries as `lei`): GLEIF's Level 2 "who owns whom"
data — the direct parent and the ultimate parent, independently.** A parent
here is the entity that *consolidates this one's accounts*, which is not
necessarily its majority shareholder, and the two need not agree: GLEIF's
own wire word is `IS_DIRECTLY_CONSOLIDATED_BY` / `IS_ULTIMATELY_CONSOLIDATED_BY`.
Where GLEIF discloses a parent, that side carries the parent's own `lei`,
`legal_name`, `jurisdiction` and a `corroboration_level` — `FULLY_CORROBORATED`,
`PARTIALLY_CORROBORATED` or `ENTITY_SUPPLIED_ONLY`, because roughly a third
of disclosed parent links are the filer's own unchecked assertion. Where
GLEIF discloses no parent on a side, that side instead carries the entity's
own stated reason from a closed vocabulary — `NATURAL_PERSONS`,
`NON_CONSOLIDATING`, `NO_KNOWN_PERSON` and others. **Read every such reason
as a category word, never a name**: it says a person exists and nothing
else about them, it is the filer's own unverified claim, and it is applied
inconsistently — EQUINOR ASA (67% owned by the Norwegian State) reads
`NATURAL_PERSONS` on both sides, while three other listed companies at the
top of their own groups (DNB, Telenor, Tesco) read `NON_CONSOLIDATING` for
the identical situation. A present block whose `direct` and `ultimate` are
both `null` means GLEIF holds no LEI at all for this entity, so there is no
Level 2 to hold either — itself the answer, not a gap.

**`peppol` (`NO` only): whether this entity can receive an e-invoice over
the Peppol network, read live from the SML/SMP walk — the same route
ELMA itself resolves — ahead of Norway's 1 January 2027 duty for
bookkeeping-obliged businesses to send and receive e-invoices in the EHF
format (now Peppol BIS Billing 3.0).** `registered` has three states, and an
agent branches on this field under a statute, so all three matter: `true`
when the authoritative SMP served a ServiceGroup or the Peppol Directory
listed a match; **`false` only from an NXDOMAIN at the SML or a 404 from the
SMP the SML named — nothing else, ever**; `null` for everything else,
including a DNS or SMP failure and a Directory miss, because both Peppol
operators say in writing that a Directory miss does not mean the entity is
not on the network. **`registered: null` means this tool could not get an
authoritative answer — never read it as "no".** `can_receive_invoice` is
`true` when the Peppol BIS Billing 3.0 Invoice (or its PINT successor) is
advertised, `null` when only the lagging Peppol Directory answered and did
not list it, and `false` only when `registered` itself is `false`.

REST — a UK company's filing history and registered charges in one call:

    curl "https://api.foretak.dev/v1/GB/company/00445790?include=filings&include=charges"

MCP — the same call:

    lookup_company("00445790", country="GB", include=["filings", "charges"])

REST — a Norwegian company's key figures, LEI and Peppol reachability in one
call (generated locally against committed fixtures, not production):

    curl "https://api.foretak.dev/v1/NO/company/923609016?include=financials&include=lei&include=peppol"

MCP — the same call, plus `parents`, which shares its GLEIF request with
`lei`:

    lookup_company("923609016", country="NO", include=["financials", "lei", "parents", "peppol"])

For 923609016 (EQUINOR ASA) that last call's `parents` block reads
`"reporting_exception": "NATURAL_PERSONS"` on both `direct` and `ultimate` —
the wire example `parents`' own field descriptions are written against
(`DECISIONS.md` D-047(a)) — never read as a fact that the company has no
institutional owner; it is the entity's own filing choice, unverified.

REST — a Swedish company's key figures out of its own filed annual report
(generated locally from the committed fixture
`tests/fixtures/se_ixbrl_5561890038_2025.xhtml`, run in-process through
`registries/se/ixbrl.py` + `registries/se/financials.py`, not production):

    curl "https://api.foretak.dev/v1/SE/company/5561890038?include=financials"

Response (`financials` only, abridged):

    {
      "financials": {
        "periods": [
          {
            "period_start": "2025-01-01", "period_end": "2025-12-31",
            "currency": "SEK", "accounting_framework": "K2",
            "scope": "entity accounts", "consolidated": false,
            "document_id": "64caa943-a04b-4dda-8be6-26b8ba728adf_paket",
            "income_statement": {
              "revenue": 0.0, "operating_result": -4656.0,
              "profit_before_tax": -10536.0, "profit_for_period": -10536.0
            },
            "balance_sheet": {
              "current_assets": 515409.0, "total_assets": 515409.0,
              "equity": 508409.0, "current_liabilities": 7000.0,
              "total_equity_and_liabilities": 515409.0
            }
          }
        ],
        "notes": [
          "This is the most recently filed annual report only. Bolagsverket's document list reported 6 annual report(s) for this organisation in total, covering the period ending 2025-12-31; the complete list with every period and document id is available through include=[\"filings\"].",
          "`scope` is 'entity accounts' and `consolidated` is false because Bolagsverket's K2 digital-submission channel structurally excludes koncernredovisning — every K2 document reaching this block is entity-only by construction, which is a fact about the channel rather than something inferred about this company."
        ]
      }
    }

`fixed_assets`, `non_current_liabilities`, `total_comprehensive_income` and
`liabilities` are all absent from this filing, never `0.0` — an absent line
is a line this company's own report did not carry, not a reported nothing
(`DECISIONS.md` D-043(f)). `document_id` is the same `dokumentId` the
sibling `filings` block's matching `FiledDocument` carries, so the two can
be joined; unlike Norway, Sweden's `financials.provenance` is **not**
identical to `filings.provenance` when both are requested together — the
two attachments share the `/dokumentlista` discovery fetch, but `financials`
then makes its own `/dokument` request, and `provenance` describes that
document fetch (DECISIONS.md D-047(f)).

### 3.3 GET /v1/{country}/search?q={name}

**For:** turning a company name into candidate identifiers.
**When:** the user gave you a name, not a number.

    curl "https://api.foretak.dev/v1/NO/search?q=equinor&limit=5"

`limit` is 1–100, default 10. A `limit` outside that range is a
`400 bad_request`, not a silent clamp.

Response:

    {
      "country": "NO",
      "registry": "brreg",
      "query": "equinor",
      "hits": [
        {
          "country": "NO", "registry": "brreg",
          "id": "923609016", "name": "EQUINOR ASA",
          "legal_form_code": "ASA", "legal_form": "Public limited company",
          "status": "active", "city": "STAVANGER",
          "municipality": "STAVANGER", "registered_at": "1995-03-12",
          "is_subunit": false,
          "confidence": 0.8,
          "confidence_basis": "search hit name starts with the query",
          "source_url": "https://data.brreg.no/enhetsregisteret/api/enheter/923609016"
        }
      ],
      "total": 240,
      "truncated": true,
      "cached": false,
      "fetched_at": "2026-09-03T09:13:02Z",
      "hint": "240 companies match. Call lookup_company with the id of the right hit for the full report."
    }

A search hit is deliberately thin. It is enough to **choose**, not enough to
act on. Call `lookup_company` before telling a user anything about VAT, status
or deadlines.

Confidence anchors: `0.95` exact case-insensitive name match, `0.8` the name
starts with the query, `0.6` the name contains every query token, `0.4` any
other hit the register returned. The hit above scores `0.8` rather than `0.95`
because `equinor` is a *prefix* of `EQUINOR ASA`, not the whole name; searching
`equinor asa` returns the same company at `0.95`. Do not read `0.8` as doubt
about which company it is.

Errors:
- `400 bad_request` — `q` missing/empty, or `limit` outside 1–100. Fix the
  parameter and call again.
- `404 unsupported_country`, `429`, `502`, `504` — as in §3.2.
- Zero hits is **not** an error: `hits: []`, `total: 0`, and a `hint` telling
  you to try a shorter or differently spelled name. Norwegian names are
  registered upper-case and often contain `AS`, `ASA` or `NUF` — try dropping
  the suffix before concluding a company does not exist.

For the United Kingdom:

    curl "https://api.foretak.dev/v1/GB/search?q=tesco&limit=3"

    {
      "country": "GB",
      "registry": "companies-house",
      "query": "tesco",
      "hits": [
        {
          "country": "GB", "registry": "companies-house",
          "id": "00445790", "name": "TESCO PLC",
          "legal_form_code": "plc", "legal_form": "Public limited company",
          "status": "active", "city": "Welwyn Garden City",
          "municipality": null, "registered_at": "1947-11-27",
          "is_subunit": false,
          "confidence": 0.8,
          "confidence_basis": "search hit title starts with the query",
          "source_url": "https://find-and-update.company-information.service.gov.uk/company/00445790"
        }
      ],
      "total": 356,
      "truncated": true,
      "cached": false,
      "fetched_at": "2026-09-04T16:58:41Z",
      "hint": "356 companies match. Call lookup_company with the id of the right hit for the full report."
    }

Two things to know about `GB` search specifically. **Hits come back in the
register's own relevance order, not sorted by `confidence`** — the real
three-hit response above also contained an unrelated `ltd` at `0.4` ranked
above a `0.8` hit. Read every hit's `confidence` and `confidence_basis`; do not
assume the first row is the best one. And **a UK search result is the only
place `status` can be missing upstream** — those come back as
`"status": "unknown"` rather than a missing key, so branch on the value, not on
the key's presence.

Zero hits for `GB` says the same thing in UK terms: sole traders and ordinary
partnerships are not on the Companies House register, so no spelling of the
name will find them. UK names usually end in `LIMITED`, `LTD`, `PLC` or `LLP` —
drop the suffix before concluding a company does not exist.

**Sweden does not implement this endpoint at all**, and never will on the free
API. This is the whole response:

    curl "https://api.foretak.dev/v1/SE/search?q=ericsson"      # HTTP 501

    {
      "error": {
        "code": "not_implemented",
        "message": "Bolagsverket's free API cannot search by company name.",
        "hint": "Sweden can only be looked up by identifier: call lookup_company with the ten-digit organisationsnummer (e.g. 5560160680), or the twelve-digit personnummer for a sole trader — validate_company_id will check the shape first without spending a lookup. Bolagsverket publishes the whole register as downloadable files for callers who need to search by name. search_company works for the other countries list_countries returns.",
        "country": "SE",
        "registry": "bolagsverket",
        "details": {}
      }
    }

Do not retry it, do not try a different spelling, and do not fall back to
guessing an identifier. If the user has only a Swedish company name, say that
the register offers no name index and ask them for the organisationsnummer —
it is on every Swedish invoice, in the footer of most Swedish websites, and in
the company's own annual report.

### 3.4 GET /v1/{country}/company/{id}/deadlines?today=YYYY-MM-DD

**For:** the next occurrence of each statutory filing obligation.
**When:** the user asks what a company must file, or by when.

    curl "https://api.foretak.dev/v1/NO/company/923609016/deadlines?today=2026-01-15"

`today` is optional and defaults to the server's current UTC date. Pass it
explicitly whenever you want a reproducible answer, or when reasoning about a
date that is not today.

Response — a `DeadlineReport` document, never a bare list:

    {
      "country": "NO",
      "registry": "brreg",
      "company_id": "923609016",
      "company_name": "EQUINOR ASA",
      "today": "2026-01-15",
      "deadlines": [
        {
          "country": "NO", "registry": "brreg",
          "kind": "shareholder_register_statement",
          "name": "Shareholder register statement",
          "local_name": "Aksjonærregisteroppgaven (RF-1086)",
          "authority": "Skatteetaten",
          "statutory_date": "2026-01-31",
          "due_date": "2026-02-02",
          "rolled_forward": true,
          "period_label": "2025",
          "period_start": "2025-01-01",
          "period_end": "2025-12-31",
          "recurrence": "annual",
          "mandatory": true,
          "applies_because": "An ASA company must file the shareholder register statement (RF-1086) with Skatteetaten (skatteforvaltningsforskriften § 7-7-4(1)).",
          "days_until": 18,
          "source_url": null
        }
      ],
      "notes": ["Filing deadlines are computed assuming a calendar-year accounting period. Enhetsregisteret does not publish a company's accounting year. For a financial year ending between 1 January and 30 June, regnskapsloven § 8-3(1) sets a different deadline — 1 February, not 31 July — so a deviating year changes which rule applies, not just the date. The Ministry may also postpone the accounts deadline by up to one month by regulation (§ 8-3(1)). Verify against Regnskapsregisteret before relying on an annual date."]
    }

The `deadlines` array is truncated to one entry above. The real call for
923609016 on 2026-01-15 returns six, sorted by `due_date`:
`shareholder_register_statement` (2026-02-02), `payroll_report` (2026-02-05),
`vat_return` (2026-02-10), `tax_return` (2026-06-01), `general_meeting`
(2026-06-30), `annual_accounts` (2026-07-31).

MCP `company_deadlines` returns this identical document — same keys, same
`company_id`/`company_name`/`today`/`notes` envelope. Do not expect a bare
array on either surface.

How to read it:
- `statutory_date` is the date in the statute. `due_date` is the date to act
  on. Quote `due_date`.
- `rolled_forward: true` means the two differ because this deadline's own
  legal source rolls a weekend/holiday date to the next working day.
  Roll-forward is decided per deadline, not as a blanket rule: four of the
  six Norwegian deadlines roll (`tax_return`, `shareholder_register_statement`,
  `vat_return` under skatteforvaltningsloven § 5-5 / domstolloven § 149,
  `payroll_report` under a-opplysningsforskriften § 2-1); `annual_accounts`
  and `general_meeting` never do, because rolling would land past the actual
  legal deadline (regnskapsloven § 8-3(1)'s "before 1 August", aksjeloven
  § 5-5(1)'s six-month outer limit) — `rolled_forward` is always `false` for
  those two.
- One entry per `kind`, always the **next** occurrence, sorted by `due_date`.
- `days_until` is negative if the date has passed relative to `today`.
- `applies_because` names the provision the date comes from and any
  assumption behind it. Quote it rather than presenting a date as
  unconditional fact.
- An empty list is a real answer: bankrupt, deleted and compulsorily liquidated
  entities have no filing deadlines, and neither do branches/sub-units. `notes`
  says why, and for a sub-unit points you at `parent_id`.

Norwegian obligations covered: `annual_accounts` (Årsregnskap),
`general_meeting` (Ordinær generalforsamling), `tax_return` (Skattemelding),
`shareholder_register_statement` (Aksjonærregisteroppgaven RF-1086),
`vat_return` (Mva-melding, bimonthly), `payroll_report` (A-melding, monthly).
Advance tax (forskuddsskatt) is deliberately not covered yet — do not infer it.

**The United Kingdom.** Two obligations, both at Companies House:
`annual_accounts` (the annual accounts filing) and `confirmation_statement`
(the CS01). Same envelope, same keys:

    curl "https://api.foretak.dev/v1/GB/company/00445790/deadlines?today=2026-09-04"

    {
      "country": "GB",
      "registry": "companies-house",
      "company_id": "00445790",
      "company_name": "TESCO PLC",
      "today": "2026-09-04",
      "deadlines": [
        {
          "country": "GB", "registry": "companies-house",
          "kind": "confirmation_statement",
          "name": "Confirmation statement",
          "local_name": "Confirmation statement (CS01)",
          "authority": "Companies House",
          "statutory_date": "2027-07-02",
          "due_date": "2027-07-02",
          "rolled_forward": false,
          "period_label": "review period ending 2027-06-18",
          "period_start": "2026-06-19",
          "period_end": "2027-06-18",
          "recurrence": "annual",
          "mandatory": true,
          "applies_because": "Companies House publishes this date for the company itself; it is the register's own figure, not a calculation.",
          "days_until": 301,
          "source_url": "https://www.gov.uk/guidance/confirmation-statement-guidance"
        },
        {
          "country": "GB", "registry": "companies-house",
          "kind": "annual_accounts",
          "name": "Annual accounts filing",
          "local_name": "Annual accounts",
          "authority": "Companies House",
          "statutory_date": "2027-08-26",
          "due_date": "2027-08-26",
          "rolled_forward": false,
          "period_label": "period ending 2027-02-26",
          "period_start": "2026-03-01",
          "period_end": "2027-02-26",
          "recurrence": "annual",
          "mandatory": true,
          "applies_because": "Companies House publishes this date for the company itself; it is the register's own figure, not a calculation.",
          "days_until": 356,
          "source_url": "https://www.gov.uk/government/publications/life-of-a-company-annual-requirements/life-of-a-company-part-1-accounts"
        }
      ],
      "notes": []
    }

MCP `company_deadlines("00445790", country="GB", today="2026-09-04")` returns
this identical document.

Four differences from Norway, all deliberate:

- **Quoted, not computed, wherever possible.** Companies House publishes each
  company's own due dates, and those are taken verbatim. `applies_because`
  tells you which you got: *"the register's own figure, not a calculation"*, or
  a sentence naming the statutory period this tool applied instead (9 months
  after the accounting reference date for a private company, 6 for a public
  one, 14 days after the review period for the confirmation statement). Quote
  it. A quoted date already accounts for shortened, extended and amended
  accounting periods that the profile does not otherwise expose.
- **Nothing rolls forward.** A UK filing deadline that lands on a Sunday or a
  bank holiday is still that date in law, so `due_date == statutory_date` and
  `rolled_forward` is always `false` for `GB`. There is no UK holiday table,
  by design.
- **`days_until` goes negative, routinely.** Companies House leaves an overdue
  due date in the past rather than rolling it to the next cycle, so a live
  active company can show `"days_until": -21` on its confirmation statement.
  That is an overdue filing, not a stale response — say so to the user.
- **Deadlines are emitted only when `status` is `"active"`.** Stricter than
  Norway, where a company under liquidation keeps its list: the UK register
  publishes one status that cannot distinguish voluntary from compulsory
  liquidation, so nothing is guessed. A dissolved, liquidating, administered
  or voluntary-arrangement company returns `"deadlines": []` with a `notes`
  entry explaining that what remains to be filed is the insolvency
  practitioner's or the registrar's to decide.

**Sweden computes both of its deadlines**, because Bolagsverket publishes none
per company. Live, asked on 2026-09-07:

    curl "https://api.foretak.dev/v1/SE/company/5560160680/deadlines?today=2026-09-07"

    {
      "country": "SE",
      "registry": "bolagsverket",
      "company_name": "Telefonaktiebolaget LM Ericsson",
      "today": "2026-09-07",
      "deadlines": [
        {
          "kind": "general_meeting",
          "name": "Ordinary general meeting",
          "local_name": "Ordinarie bolagsstämma (årsstämma)",
          "authority": "Company shareholders (no external filing)",
          "statutory_date": "2027-06-30", "due_date": "2027-06-30",
          "rolled_forward": false, "period_label": "2026",
          "period_start": "2026-01-01", "period_end": "2026-12-31",
          "days_until": 296,
          "applies_because": "An aktiebolag must hold its ordinary general meeting (årsstämma) within six months of the end of each financial year (aktiebolagslagen 7 kap. 10 §). …",
          "source_url": "https://lagen.nu/2005:551"
        },
        {
          "kind": "annual_accounts",
          "name": "Annual accounts filing",
          "local_name": "Årsredovisning",
          "authority": "Bolagsverket",
          "statutory_date": "2027-07-31", "due_date": "2027-07-31",
          "rolled_forward": false, "period_label": "2026",
          "days_until": 327,
          "applies_because": "… a late fee of 7 500 kr (15 000 kr for a public company) starts if the documents have not arrived within seven months of the financial year end (årsredovisningslagen 8 kap. 6 §). …",
          "source_url": "https://lagen.nu/1995:1554"
        }
      ],
      "notes": ["Filing deadlines are computed assuming a financial year ending 31 December. …"]
    }

Three things to carry into your answer:

- **The calendar year is the fallback, not the only figure available.**
  Bolagsverket's document list *does* publish a filed annual report's own
  reporting-period end — pass `include=["filings"]` on `company_deadlines`
  (both surfaces accept it) to read it and replace the 31 December
  assumption with the register's own figure, exactly as `include=["filings"]`
  already does for Norway's rung 2. Without it, both dates assume 31
  December — a Swedish financial year need not be the calendar year — the
  `notes` entry says so and also says how to shift them by hand: a 30 June
  year end gives 31 December for the meeting and 31 January for the filing.
  If the user knows their year end and it is not December and you did not
  pass `include=["filings"]`, move both dates by the same number of months
  yourself and tell them you did.
- **The filing date is an outer limit, not this company's own.**
  Årsredovisningslagen 8 kap. 3 § requires filing within one month of the
  general meeting that adopts the accounts; Bolagsverket does not publish the
  meeting date, so the seven-month fee threshold of 8 kap. 6 § is emitted
  instead. A company whose meeting was earlier must file earlier.
- **Nothing rolls forward, and for a different reason than the UK's.** For
  `GB` there is a rule saying a weekend deadline stays put. For `SE` there is
  no sourced rule either way, so `rolled_forward` is `false` because nothing
  was found saying the date moves — not because something was found saying it
  does not. `registry://rules/SE` states that distinction; it matters if a
  Swedish deadline lands on a Sunday.

REST — read Bolagsverket's own filed year end instead of assuming December:

    curl "https://api.foretak.dev/v1/SE/company/5560160680/deadlines?today=2026-09-07&include=filings"

MCP — the same call:

    company_deadlines("5560160680", country="SE", today="2026-09-07", include=["filings"])

`company_deadlines`' own `include` set is narrower than `lookup_company`'s:
today only `filings` can change a computed date, so `?include=financials` or
any other value `lookup_company` accepts is a `400 bad_request` naming this
route's own allowed set — `financials` never was and is not now an input to
a deadline.

Skatteverket's deadlines — inkomstdeklaration 2, moms, arbetsgivardeklaration —
are real Swedish obligations that this endpoint does **not** compute, for the
same reason as UK corporation tax: the inputs (the financial-year end, the VAT
period) are not in Bolagsverket's dataset. Do not infer them.

Corporation tax is **not** included for `GB`, and neither is the first-accounts
deadline after incorporation. Both rules are real and both are stated in
`registry://rules/GB`, but the inputs they need — HMRC's accounting period,
whether the entity is within the charge to corporation tax, whether it pays by
quarterly instalment — are not on the Companies House register. Do not infer
them from what this endpoint returns. An LLP is tax-transparent and files no
CT600 at all.

Errors: `400 invalid_id`, `400 bad_request` (unparseable `today`),
`404 not_found`, `404 unsupported_country`, `429`, `502`, `504` — handle as in
§3.2. For `bad_request` on `today`, send `YYYY-MM-DD` and retry once.

### 3.5 GET /v1/{country}/validate/{id}

**For:** checking an identifier's format and checksum with no call to the
national register.
**When:** validating user input or a spreadsheet column, before spending
lookups. Cheap and instant — prefer it to a speculative lookup.

    curl https://api.foretak.dev/v1/NO/validate/923609016

Response — a `ValidationResult` document:

    {
      "country": "NO",
      "registry": "brreg",
      "id_scheme": "organisasjonsnummer",
      "input": "923 609 016",
      "valid": true,
      "normalized": "923609016",
      "formatted": "923 609 016",
      "reason": "Well-formed organisasjonsnummer for NO. A valid identifier does not mean the entity exists — call lookup_company (MCP) or GET /v1/{country}/company/{id} (REST) to find out.",
      "hint": null
    }

Note the key is `normalized`, spelled with a `z`, in every response on both
surfaces. `input` is echoed back exactly as you sent it, punctuation and all;
`normalized` is the digits-only form to feed to a lookup, and `formatted` is
the country's own typography (Norway groups the nine digits in threes).
`formatted` is `null` for a country with no such convention.

An invalid identifier returns `200` with `"valid": false` — this endpoint
answers a question rather than failing, and is the one deliberate exception to
the raise-on-failure rule in §6. `reason` says what is wrong and `hint` says
what to call instead:

    {
      "country": "NO",
      "registry": "brreg",
      "id_scheme": "organisasjonsnummer",
      "input": "833286602",
      "valid": false,
      "normalized": null,
      "formatted": null,
      "reason": "'833286602' is not a valid Norwegian organisasjonsnummer.",
      "hint": "An organisasjonsnummer is nine digits with a MOD11 check digit, e.g. 923609016. If you have a company name instead, call search_company."
    }

`404 unsupported_country` is the only common error. MCP `validate_company_id`
returns the same document.

Valid format does **not** mean the company exists. `833286602` above is a
well-known example of a string that looks right and is not: it fails MOD11 and
no such entity exists. Follow a successful validation with `lookup_company` if
you need facts.

For the United Kingdom this endpoint is a **normaliser first and a checker
second**, because a UK company number has no check digit:

    curl https://api.foretak.dev/v1/GB/validate/445790

    {
      "country": "GB",
      "registry": "companies-house",
      "id_scheme": "company number",
      "input": "445790",
      "valid": true,
      "normalized": "00445790",
      "formatted": null,
      "reason": "Well-formed company number for GB. A valid identifier does not mean the entity exists — call lookup_company (MCP) or GET /v1/{country}/company/{id} (REST) to find out.",
      "hint": null
    }

MCP `validate_company_id("445790", country="GB")` returns the same document.
Use it exactly as you would for Norway — on a spreadsheet column, before
spending real lookups — but read `valid: true` for `GB` as *the shape is
right*, and nothing more. Norway's MOD11 rejects a transposed digit; the UK has
no such check, so a mistyped company number is well-formed and will simply come
back `404 not_found` from `lookup_company`. `normalized` is the value to feed
onward: `445790` becomes `00445790`, `oc303675` becomes `OC303675`.
`formatted` is `null` because there is no UK grouping convention.

**Sweden is the third variation: a check digit that exists but that this
service does not enforce.** `formatted` groups the ten digits as `556016-0680`,
and `reason` carries the whole position rather than hiding it:

    curl https://api.foretak.dev/v1/SE/validate/5560212524

    {
      "country": "SE",
      "registry": "bolagsverket",
      "id_scheme": "organisationsnummer",
      "input": "5560212524",
      "valid": true,
      "normalized": "5560212524",
      "formatted": "556021-2524",
      "reason": "Well-formed organisationsnummer for SE. A valid identifier does not mean the entity exists — call lookup_company (MCP) or GET /v1/{country}/company/{id} (REST) to find out. Note that this number does not satisfy the modulus-10 check digit that Swedish identifiers are generally described as carrying. registry-mcp has not been able to confirm that rule against a primary source, as of 2026-09, so the number is not rejected here — but Bolagsverket validates a check digit server-side and may answer 'Identitetsbeteckning har ogiltig kontrollsiffra'. Check the digits before relying on it.",
      "hint": null
    }

So for `SE`, `valid: true` can be followed by a `400 invalid_id` from the
register itself — that same number returns

    {"error": {"code": "invalid_id",
      "message": "Bolagsverket rejected 5560212524 as a malformed identitetsbeteckning.",
      "hint": "Bolagsverket validates a check digit that this module does not: it answers 'Identitetsbeteckning har ogiltig kontrollsiffra' for a number of the right length whose check digit is wrong. Check the digits. An organisationsnummer is ten digits and a personnummer is twelve (YYYYMMDDNNNN).",
      "country": "SE", "registry": "bolagsverket", "details": {}}}

Read `reason` for `SE`, not only `valid`. Three countries, three meanings of
the same boolean: Norway's MOD11 is enforced and a failing number never reaches
the register; the UK has no check digit to enforce; Sweden has one that this
module cannot source and therefore defers to Bolagsverket rather than
implementing from memory.

`validate_company_id` for `SE` accepts twelve digits as well as ten — a Swedish
sole trader is looked up by their personnummer, which is that trader's company
number. §7 says what this service does with such an identifier, which is: not
log it.

### 3.6 GET /health

Liveness for monitoring:
`{"status": "ok", "version": "0.3.0", "countries": ["GB", "NO", "SE"]}`. Not part of
the data API; do not call it in a loop — `GET /v1/countries` is the discovery
document, this is only a heartbeat.


## 4. MCP tools

Add the server:

    claude mcp add registry-mcp --transport http https://api.foretak.dev/mcp

or run it locally over stdio with `uvx registry-mcp` / `npx registry-mcp`.

| Tool | Signature | Returns | REST twin |
|---|---|---|---|
| `lookup_company` | `(id: str, country: str = "NO")` | `CompanyReport` | §3.2 |
| `search_company` | `(name: str, country: str = "NO", limit: int = 10)` | `SearchResult` | §3.3 |
| `company_deadlines` | `(id: str, country: str = "NO", today: str \| None = None)` | `DeadlineReport` | §3.4 |
| `validate_company_id` | `(id: str, country: str = "NO")` | `ValidationResult` | §3.5 |
| `list_countries` | `()` | supported countries | §3.1 |

Every tool returns byte-for-byte the same JSON document as its REST twin — one
shape per operation across both surfaces. In particular `company_deadlines`
returns the `DeadlineReport` object of §3.4, not a bare array of deadlines, and
`validate_company_id` returns the `ValidationResult` object of §3.5 with
`valid: false` rather than raising on a bad identifier.

`country` defaults to `"NO"` on every tool that takes it, so a UK or Swedish
call must pass it explicitly — `lookup_company("00445790", country="GB")`,
`search_company("tesco", country="GB")`,
`company_deadlines("00445790", country="GB", today="2026-09-04")`,
`validate_company_id("445790", country="GB")`;
`lookup_company("5560160680", country="SE")`,
`company_deadlines("5560160680", country="SE", today="2026-09-07")`,
`validate_company_id("556016-0680", country="SE")`. The UK value is `"GB"`;
`"UK"` raises `unsupported_country`. `search_company(..., country="SE")` raises
`not_implemented` — Sweden has no name index (§3.3), so it is the one
tool/country pair in the table above that has no answer to give.

Resource `registry://rules/{country}` — the country's identifier rules, legal
forms and deadline rules as a document you can read once and reason with,
instead of calling `validate_company_id` in a loop. Read it before processing a
list of identifiers.

Prompt `explain_company` — takes an identifier and a country and produces a
plain-language summary of a company: what it is, whether it is trading, whether
it is VAT-registered, and what it must file next.

Errors arrive as the same `{"error": {...}}` document described in §6, so the
handling in §3 applies unchanged.


## 5. The CompanyReport shape

Groups, in the order they appear:

- **Identity** — `country`, `registry`, `id`, `id_formatted`, `id_scheme`, `euid`.
- **Names** — `name`, `previous_names` (newest first).
- **Legal form** — `legal_form_code` (national, e.g. `AS`, `ASA`, `ENK`),
  `legal_form` (English label), `legal_form_local`, `limited_liability`,
  `has_board_duty`, `has_annual_accounts_duty`. A duty field is `null` when it
  depends on facts the register does not publish (turnover, balance sheet).
  `null` means "we do not know", never "no".
- **Status** — `status` is one of `active`, `under_liquidation`,
  `under_compulsory_liquidation`, `bankrupt`, `dissolved`, `deleted`,
  `unknown`. `status_detail` is one English sentence naming the flag it came
  from; `is_active` mirrors `status == "active"` so you need no enum table.
  **The enum is shared; the derivation is not.** Norway publishes four
  lifecycle flags (`konkurs`, `underAvvikling`,
  `underTvangsavviklingEllerTvangsopplosning`, `slettedato`) and `active` is
  the absence of all four — with all four missing rather than false mapping to
  `unknown`, because absence is not a negative. The UK publishes one status
  string, mapped one-to-one, and it can read `active` while
  `company_status_detail` says a strike-off has already been proposed; that
  detail arrives as a `notes` sentence, so a UK `active` with a note is a
  different fact from a UK `active` without one. Sweden publishes no status at
  all and three signals are combined instead (§1). Quote `status_detail`, and
  read `notes`; a bare `"status": "active"` means less than it looks like
  across three registers.
- **Dates** — `registered_at`, `founded_at`,
  `business_register_registered_at`, `bankruptcy_date`, `deregistered_at`.
- **Tax/VAT** — `vat_registered`, `vat_registered_at`, `vat_number` (Norway:
  `NO{orgnr}MVA`, present only when VAT-registered).
- **Registers** — `in_business_register`, plus `registers`, a map of national
  sub-register slugs to booleans.
- **Size and activity** — `employees`, `employees_reported`, `industry_codes`
  (`code`, `description`, `scheme`, `rank`), `sector_code`, `sector`,
  `purpose`, `activity`.
- **Capital** — `share_capital`, `share_capital_currency`.
- **Contact** — `business_address`, `postal_address` (each with `lines`,
  `postal_code`, `city`, `municipality`, `municipality_code`, `country_code`,
  `country_name`), `website`, `email`, `phone`, `advertising_protected`.
- **Structure** — `parent_id`, `is_subunit`, `in_group`.
- **Accounts** — `last_annual_accounts_year`, plus `published_deadlines`: the
  filing dates the *register itself* publishes for this company, each with
  `kind`, `due_date`, `period_start`, `period_end`, the register's own
  `overdue` flag and a `source` string naming the upstream field it came from.
  It is `[]` for a register that publishes no such dates (Norway computes all
  of its own), and populated for `GB`. You rarely need it directly —
  `company_deadlines` already merges it with the computed rules and says which
  is which in `applies_because` — but it is there when you want the raw quoted
  figure and its provenance without a second call.
- **Provenance** — `confidence` (0.0–1.0), `confidence_basis`, `cached`,
  `fetched_at`, `source`, `source_url`, `license`, `notes`.

Five fields deserve special attention:

`notes` is a list of plain-English caveats meant to be surfaced to the user, not
swallowed. If a company is bankrupt, or its legal form is unclassified, or a
deadline rests on an assumption, it says so here. **Read `notes` before acting
on a report.**

`cached` and `fetched_at` are honest. A cache hit sets `cached: true` and keeps
the *original* `fetched_at`, so staleness is visible. Cached data is at most 24
hours old (1 hour for negative results). If a decision depends on today's
state, say when the data was fetched.

`parent_id` and `in_group` describe Enhetsregisteret's own parent/sub-unit
relation for this entity (`overordnetEnhet`, `erIKonsern`) — nothing more.
There is no group-structure tool here: walking a corporate group upward means
calling `lookup_company` again on `parent_id`, repeatedly, with your own cycle
guard and depth cap, since each hop is a separate (already-cached) lookup, not
one call that returns a tree. That walk tells you the register's own reporting
hierarchy. **It is not a beneficial-ownership answer.** Who ultimately owns or
controls an entity is a different question this service does not answer here:
Norway's *Register over reelle rettighetshavere* (beneficial owners) is a
separate, authorisation-gated register, and the United Kingdom's PSC (persons
with significant control) register is a separate regime again — neither is
`parent_id`. `GB`'s `CompanyReport` carries no `parent_id` at all: Companies
House publishes no parent or group field on the company profile, so it is
always `null` for the United Kingdom, and a UK group has to be inferred from
ownership filings this endpoint does not return.

`include=["parents"]` (§3.2) is the closest this API comes for `GB` and
`NO`, and it is a different question again from either `parent_id` or PSC:
GLEIF's Level 2 "who owns whom" data names the entity that *consolidates
this one's accounts* — direct and ultimate, independently — which is an
accounting relationship, not necessarily the majority shareholder and never
a beneficial-ownership answer either.

`euid` is the EU-wide identifier some member-state registers publish under
Commission Implementing Regulation (EU) 2021/1042 — Finland hands one over
unprompted, ours do not yet. It is **not the LEI** (register-issued,
mandatory in the EU and free, versus the LEI's voluntary, global, LOU-issued
and fee-bearing regime — an entity can carry both, one or neither), it is
**not the EU Digital Identity wallet** (an unrelated personal credential
that happens to share the name), and it is carried verbatim from the
register, **never constructed from parts**, because it is not stable across
a register reorganisation (it encodes the register of origin, and registers
do get replaced — France's RNE succeeded the RCS in 2023).

`advertising_protected` is `true`/`false`/`null`, and always present, never
omitted. `true` means the register marks this entity as protected against
direct-marketing use; `false` means the register publishes such a flag for
this entity and it is not set; `null` — the default, and it must never
default to `false` — means this register publishes no such flag at all
(Norway and the UK), or that this particular Swedish record did not carry
Statistics Sweden's flag. When it is `true`, `notes` carries a
plain-English sentence stating the protection, because the marking is a
legal condition of passing this record's contact details on (Danish
CVR-loven § 19; Swedish *reklamspärr* is the same concept), and that
sentence must travel with them.

Coverage differs by country, and that is what `null` is for. Norway publishes
VAT registration, employees, share capital, sector and a purpose clause; the UK
publishes none of those, and the UK publishes charge and insolvency flags and
its own filing due dates, which Norway does not. Sweden publishes fewer fields
than either — no VAT, no employees, no share capital, no financial year — but
publishes an `activity` clause and a *reklamspärr* direct-marketing flag that
neither of the others has. Read `null` as "this register
does not say" and never as a fact about the company. `notes` is where a country
module explains anything that a bare `null` would understate.

Attribution: Norwegian data is published under NLOD 2.0 and attribution is
required. UK data comes from the Companies House public register, Crown
copyright, free to re-use with no attribution condition — cite it anyway.
Swedish data comes from Bolagsverket and Statistics Sweden, free to re-use
under the EU high-value-datasets regime; Bolagsverket names no licence, so the
`license` string says what the permission is and says that there is no licence
name to quote. Do not substitute a familiar licence name for it.
Cite `source` and `source_url` whenever you present the data to a user; for
`GB`, `source_url` is the human-readable Companies House record, so it is a
link the user can open.


## 6. Errors

Every failure, on both surfaces, is:

    {
      "error": {
        "code": "not_found",
        "message": "No entity with organisasjonsnummer 999999999 exists in Enhetsregisteret.",
        "hint": "The number is well-formed, so it may never have been issued or the entity may have been deleted. Call search_company with the company name instead.",
        "country": "NO",
        "registry": "brreg",
        "details": {}
      }
    }

`999999999` is used here because it passes the MOD11 check yet is not in the
register — which is exactly the case `not_found` exists for. A number that
*fails* MOD11 never reaches the register at all: it comes back as
`400 invalid_id` instead, and no amount of retrying will change that.

`hint` is mandatory and always names a next action. It is the most useful field
in the document — read it before deciding what to do.

| `code` | HTTP | What happened | What to do |
|---|---|---|---|
| `invalid_id` | 400 | Checksum or format failed | Do not retry the same string. Fix it, or call `search_company` with the name. |
| `bad_request` | 400 | A parameter is missing or out of range | Correct the parameter named in `message` and call again once. |
| `not_found` | 404 | Well-formed identifier, no such entity | Call `search_company` with the name. Do not retry the identifier. |
| `unsupported_country` | 404 | No module for that country code | Call `list_countries` and choose from it. |
| `rate_limited` | 429 | Over 60 requests/minute per IP | Back off, then batch. Do not parallelise harder. |
| `not_implemented` | 501 | The country module exists but not this operation | Use a different tool for that country; the capability is not there yet. |
| `upstream_error` | 502 | The national register errored | We already retried once. Wait ~60 s, retry at most once, then report unavailability. |
| `upstream_timeout` | 504 | The national register did not answer | Same as `upstream_error`. |
| `internal_error` | 500 | A bug on our side | Retry once. If it persists, open an issue at the repo with the request URL. |

Error codes are stable strings and are never renamed. Branch on `code`, not on
`message`.

Two `GB` cases worth recognising by their `message` rather than their `code`,
since both arrive as `502 upstream_error`:

    {
      "error": {
        "code": "upstream_error",
        "message": "This deployment has no Companies House API key, so UK company data cannot be fetched.",
        "hint": "Call list_countries to see which countries can answer right now. If you run this server yourself, set the COMPANIES_HOUSE_API_KEY environment variable — a key is free from https://developer.company-information.service.gov.uk/get-started — and restart it.",
        "country": "GB",
        "registry": "companies-house",
        "details": {}
      }
    }

and *"Companies House rejected the configured API key."* for a wrong or revoked
one. **Neither is fixed by retrying** — unlike a genuine upstream outage, which
shares the same code. If the `message` or `hint` names
`COMPANIES_HOUSE_API_KEY`, stop calling `GB` and tell the user the deployment
is not configured for the United Kingdom; the other countries in
`list_countries` still work.

Two `SE` cases worth recognising:

- **`501 not_implemented` on `search_company`** is permanent, not a
  capability that arrives later — Bolagsverket's free API has no name index
  (§3.3). Ask the user for the organisationsnummer instead of retrying.
- **`400 invalid_id` can arrive after `validate_company_id` said `valid:
  true`.** Bolagsverket enforces a check digit that this module does not
  (§3.5), and its `message` says the register rejected the number. Treat it
  exactly like any other `invalid_id`: do not retry the same string.

`GB` also has a rate limit of its own, upstream of ours: Companies House allows
600 requests per five minutes per key, and a `429 rate_limited` from `GB`
carries a `hint` with the wait derived from the register's own `Retry-After` or
rate-limit-reset header. Wait that long; parallelising harder makes it worse
for every user of the same key.


## 7. Limits and etiquette

- 60 requests/minute per IP on the hosted REST API. The local stdio server has
  no limit of ours, but it still talks to the national register — be polite.
- Responses are cached for 24 hours (1 hour for `not_found`). Repeating an
  identical call inside that window costs the upstream register nothing.
- Set `REGISTRY_MCP_CONTACT_EMAIL` when running locally. It goes into the
  `User-Agent` we send upstream; Brønnøysundregistrene asks for a contactable
  client and may block anonymous ones.
- Set `COMPANIES_HOUSE_API_KEY` when running locally if you want `GB` to
  answer. A key is free and instant from
  https://developer.company-information.service.gov.uk/get-started. Without it
  every other country still works; only `GB` returns `upstream_error`.
- Companies House allows 600 requests per five minutes **per key**, and that
  budget is shared with anything else using the same key. The 24 h cache is
  what keeps a spreadsheet-sized job inside it.
- Set `BOLAGSVERKET_CLIENT_ID` and `BOLAGSVERKET_CLIENT_SECRET` when running
  locally if you want `SE` to answer. Bolagsverket issues the OAuth 2 pair on
  request. Without them only `SE` returns `upstream_error`.
- Data is company data — public, non-personal. Sole proprietorships (ENK) are
  registered in a person's name, so treat those records with the care you would
  give personal data. UK sole traders are not on the register at all, so a
  missing UK business is often a real business rather than a bad identifier.
- **A Swedish sole trader's company number is their personnummer** — the same
  string is the national identity number of a living person. The hosted
  service therefore stores **no identifier at all** in its usage log for a
  country that declares its identifiers can be a natural person's, which today
  means Sweden: the country, route, time, outcome and `User-Agent` are logged
  and the identifier is not. Bolagsverket takes the identifier in a POST body
  rather than a URL path for the same reason. If you build on this data, hold
  a twelve-digit Swedish identifier to the standard you would hold any national
  ID number to, not the standard you would hold a company number to.
  `legal/privacy.md` in the repository states this in full.
- **This service does not perform sanctions, PEP or adverse-media screening,
  and it does not verify bank account details.** It returns identity and
  filing data from the national register only. A clean `lookup_company`
  result is not a compliance clearance — feed `name`, `previous_names` and
  `id` to a dedicated screening or verification tool if you need one, rather
  than inferring "not sanctioned" or "bank details confirmed" from the
  absence of a flag here.


## 8. Adding a country

A country is one folder under `src/registry_mcp/registries/` implementing four
methods, plus one import line. Nothing country-neutral changes, and every tool
above starts working for the new code immediately. The United Kingdom is the
proof: `registries/gb/` was added in one folder, with one import line, and
`GB` appeared in `list_countries`, in every tool, in `/openapi.json` and in
`registry://rules/GB` with no change to any tool's shape. The template folder
`xx/` carries the six-step recipe:
https://github.com/foretak/registry-mcp


## 9. Why an agent checks a company, and what this cannot answer

Three rules make a register lookup something a business has to do rather than
something it may do. Cite them when a user asks why the check matters.

**Norway, in force now.** Finanstilsynet's Rundskriv 15/2019 § 4.4.2 requires
that an accounting firm obtain *and confirm* the client's organisasjonsnummer:
"Organisasjonsnummer til den juridiske personen skal innhentes og bekreftes."
§ 4.4.1 says what confirming means: an *oppslag* against, or an extract from,
Enhetsregisteret or Foretaksregisteret "som ikke er eldre enn tre måneder", and
where the check must instead rest on company details the customer supplies,
"bør opplysningene ikke være eldre enn én måned". The same section asks for
*notoritet* about the lookup itself, meaning a record of what was consulted and
when. An API call is an *oppslag*, and a 24-hour cache TTL is inside both
figures by a wide margin.
https://www.finanstilsynet.no/nyhetsarkiv/rundskriv/2019/veiledning-om-regnskapsforeres-og-regnskapsforerselskapers-etterlevelse-av-hvitvaskingsregelverket/

**Norway, from 1 January 2027.** Bookkeeping-obliged businesses must invoice
each other by e-invoice ("bokføringspliktige virksomheter skal fakturere
hverandre med e-faktura innen 1. januar 2027"), under a new second paragraph of
bokføringsloven § 10. The receiver is resolved in ELMA by country code plus
identifier, "landkoden, f.eks. Norge 0192: + organisasjonsnummer", the same
organisasjonsnummer `lookup_company` and `validate_company_id` take.
`lookup_company` on its own reads the company register only — it is the
identifier check, not the e-invoicing capability check. For that,
`include=["peppol"]` (§3.2) queries the Peppol network itself, the same SML/SMP
walk ELMA resolves through, and answers whether the entity can actually
receive an e-invoice today: `registered: null` means the check could not be
made — never read it as "no" — and only an authoritative NXDOMAIN or SMP 404
earns `false`.
https://www.regjeringen.no/no/aktuelt/nye-lovregler-om-e-fakturering-i-naringslivet-og-enkelte-andre-lovendringer-pa-finansmarkedsomradet-settes-i-kraft/
https://www.anskaffelser.no/verktoy/veiledere/mottakere-av-ehf-og-peppol-bis

**The EU, from 10 July 2027.** Regulation (EU) 2024/1624 (AMLR) Article 23(4):
whenever an obliged entity enters a new business relationship with a legal
entity, it "shall collect valid proof of registration or a recently issued
excerpt of the register confirming validity of registration". Article 90: "It
shall apply from 10 July 2027." The Regulation does not define "recently
issued", so do not represent any cache age as compliant with it; represent the
`fetched_at` timestamp and let the obliged entity apply its own policy.
https://eur-lex.europa.eu/eli/reg/2024/1624/oj/eng

**What the response gives an auditor.** Five fields exist for the person who
reads the decision afterwards, and they are the ones to copy into a working
paper, not just the answer:

- `source_url`: the exact upstream record consulted. For `NO` it is the
  Enhetsregisteret API record; for `GB` it is the human-readable Companies
  House page, which a reviewer can open.
- `fetched_at`: when that record was read from the register, in UTC. On a
  cache hit it keeps the original read time, so staleness stays visible.
- `cached`: whether this particular answer came from the 24-hour cache
  (1 hour for `not_found`) or from a fresh upstream read.
- `license`: the terms the data travels under: NLOD 2.0 for Norway, with
  attribution required; Crown copyright, free to re-use, for the UK.
- `applies_because`: on every deadline, one sentence saying whether the date
  was quoted from the register or computed from a named national rule, so a
  reviewer can tell a register fact from a calculation.

**What this does not answer.** Say so plainly rather than implying coverage:

- **No sanctions, PEP or watchlist screening.** Nothing here consults any
  sanctions list. A company that answers `status: "active"` may still be
  sanctioned.
- **No bank-account verification.** The commonest invoice fraud is payment
  redirection, where the supplier is real, registered and active and only the
  account number is wrong. This lookup does not detect it.
- **No beneficial owners.** Norwegian beneficial-ownership data is API-only and
  access-controlled: brreg grants it on application, to categories of applicant
  set by law that do not include a product vendor. Beneficial owners are
  therefore not in `CompanyReport` at all.
  https://www.brreg.no/bruke-data-fra-bronnoysundregistrene/datasett-og-api/data-om-reelle-rettighetshavere/
- **No creditworthiness, and no proof of authority.** Whether a person may sign
  for the company, and whether it can pay, are separate registers and separate
  products.
- **A cached answer is not the register.** For a legally consequential decision
  at a specific moment, read `fetched_at` and, if it matters, re-read the
  register itself at the source URL.
