# AI agents (/docs/ai-agents) ## Calling the API from an agent [#calling-the-api-from-an-agent] Point an OpenAPI-driven tool layer at the REST API. The server publishes the schema it generates at `https://api.registercheck.de/v1/openapi.json`: the operations it serves and their parameters, but not the credit prices or every response shape. The [API reference](/docs/api-reference/search/searchCompanies) documents each operation in full, including its price. The agent authenticates with an API key like any other client, and each call costs the same [credits](/docs/getting-started/credits-and-billing) as the endpoint it calls. ## Giving an agent the docs [#giving-an-agent-the-docs] Every page here is available as Markdown for an agent to read: * `https://docs.registercheck.de/llms.txt` — the page index * `https://docs.registercheck.de/llms-full.txt` — the whole documentation as one file * `https://docs.registercheck.de/llms.mdx/docs//content.md` — one page as Markdown (for this page: `/llms.mdx/docs/ai-agents/content.md`) # Changelog (/docs/changelog) `/v1` changes in place. A breaking change is marked **breaking**, and the [migration guide](/docs/migration) has every one of them, old next to new. An old name that keeps working for a while says so on every response with `Deprecation` and `Sunset` headers. ## 17 October 2026 [#17-october-2026] * **The MCP server charges what the API charges.** A tool call through the [Registercheck MCP server](/docs/mcp) now costs the credits of the `/v1` call it makes, like the same call made with an API key: `search_companies` and `list_company_documents` 1 credit, `get_company`, `list_company_roles`, `list_company_shareholders`, `list_company_events` and `create_monitor` 10. Until now only `list_events` cost credits there. `POST /monitors` through a connection of the MCP server costs 10 credits, which free credits do not pay, and still counts against the companies your plan monitors. Nothing changes for API keys. * **A monitor created through the MCP server is one created with an API key.** It costs 10 credits and no longer counts against the companies your plan monitors; resuming it is never refused for that limit. `GET /account`'s `monitors` counts the monitors created in the dashboard only. * **Ten times the credits in every plan.** Starter includes 1,000 credits a month, Business 10,000 and Scale 100,000, at the same plan prices. The free plan's 50 credits stay as they are. ## 16 October 2026 [#16-october-2026] * **Resuming a monitor can be refused.** `PATCH /monitors/{monitor_id}` with `{"status": "active"}` answers `403` `monitor_limit_reached` when resuming would monitor more companies than the plan allows. Only active monitors count; monitors created with an API key never do. ## 15 October 2026 [#15-october-2026] * **The reference-data lists answer the page envelope** (**breaking**): `/legal-forms`, `/courts`, `/role-types`, `/industries`, `/event-types` and `/event-groups` now answer `{ "object": "page", "data": [...], "next_cursor": ... }` like every other list, instead of `{ "object": "list", "data": [...], "has_more": false }`. They take `limit` (1–100) and `cursor` like every other list, and `limit` is optional: omit it to receive the whole list in one page, `next_cursor` `null` — every vocabulary, `/industries`' 2,030 codes included. The data and its order are unchanged, and they stay free. The switch is hard: there is no transition period and no `Deprecation` header. See [Pagination](/docs/migration#pagination). * **How reliable `match_confidence` is, measured.** [People](/docs/concepts/people#how-reliable-is-match_confidence) publishes the precision measured per score on 2026-10-14 and recommends using person links at 0.80 or above. On 2026-10-15 the 605 links stored at 0.75, 0.70 or 0.60 whose register entry contradicted the linked person were corrected: they now point to the matching person at 0.95 or 0.90, or to a new person at 0.95, and no link is published at 0.75 or 0.70 any more. * **A monitor's event is dated when the monitor reports it.** On `GET /events?monitor_id=`, in a delivery and in a webhook body, `created_at` is the moment the monitor reported the event — the same instant in all three. For a monitor created with an API key that is within seconds of the moment the change reached Registercheck, as before. A monitor created in the dashboard reports at its check time; polling on `created_at[gte]` still finds every event. * **Destinations carry a `role`**: `own`, `account_inbox` or `route`, saying which events a destination receives. A `route`'s cadence cannot be changed (`409` `route_endpoint`). Creating a destination with a `route`'s address makes it `own`. * **Webhook deliveries of dashboard monitors.** A dashboard monitor that delivers to a webhook sends each event as a delivery with `cadence` `instant`, listed by `GET /deliveries`. Its e-mail and Slack summaries are not API deliveries and are not listed. * **The account inbox's cadence sets how often dashboard monitors check** when none was chosen for them: `instant` the plan's fastest, `daily` daily, `weekly` and `monthly` weekly. ## 9 October 2026 [#9-october-2026] * **Computed beneficial owners.** `GET /companies/{company_id}/beneficial-owners` returns a `beneficial_owner_analysis`: each natural person the filings compute as qualifying, with the path and the multiplied percentage. `status` is `complete`, `partial` or `none_found`. `gwg_above_25` (more than 25%) and `amlr_at_least_25` (25% or more) are separate flags. `fictitious_rule_applies` follows the GwG test only, and stays false when a holder was withheld, an unidentified person could still reach 25%, or that person shares a name with an identified holder and together they could reach 25%. A person is identified by one linked person record, or else by the same full name and birth date (`matched_by`). Managing directors stay on `/roles`. 25 credits. Computed from register filings; may be incomplete. Not a Transparenzregister extract. * **KYB report.** `GET /companies/{company_id}/kyb-report` assembles the company record, the first page of current roles, the first page of shareholders, the ownership tree to depth 3, and references to the shareholder list and the register extracts. 50 credits, charged once. A part that cannot be completed is a 503 and is not charged, including a shareholder list that is still being extracted. A privacy check that cannot be completed is a 500, as on the other company routes. Computed beneficial owners are not included yet. The report is computed from register filings. It is not an extract from the Transparenzregister, and it is no substitute for your own due diligence. * **Ownership tree, levels 2 and 3.** `GET /companies/{company_id}/ownership-tree` follows linked companies for the `depth` you ask (1, 2 or 3). `effective_percentage` is the product of the percentages along the path, equal to the holding's own percentage at depth 1 and rounded half up to four decimal places further down. A cycle, and a company that holds itself, is flagged and not followed. `unresolved` says where the chain ends (`foreign_entity`, `entity_not_linked`, `stock_corporation`, `no_shareholder_list`, `extraction_pending`, `depth_limit`, `cycle`, `self_holding`, `percentage_withheld`). `complete` and `truncated` say whether that list is finished. Still 10 credits. Asking still starts at most one extraction, for the company you asked about. Companies further up are read from ownership already stored. ## 8 October 2026 [#8-october-2026] * **A person's `age`.** `GET /persons/{person_id}` includes `age`: whole years as of today in Europe/Berlin, computed from `birth_date` on each request and never stored. It is `null` when the date is missing, in the future, or the age would be over 120. * **Filter companies by age.** `roles.conditions[].age` selects representatives, and Prokuristen when the condition also sets `role_type` to `["authorized_signatory"]`. Kommanditisten take `shareholders.conditions[].age` with `type: ["limited_partner"]`. Bounds are whole years from 0 to 120. An age on a role by itself, and a Kommanditist's `age` or `birth_date` by itself, answers `400`: combine it with a narrowing criterion, for example a name, a register, a location or another company filter. A shareholder criterion counts; `status` and `holder_type` alone do not. Plain shareholders have no date of birth, so `age` there answers `400`. ## October 2026: one model [#october-2026-one-model] **Breaking.** `/v1` uses one word for each thing the register holds, the same in every path, field, id and page of these docs. The [glossary](/docs/concepts/glossary) lists the words; the [migration guide](/docs/migration) lists every change. * **Ids say what they name**: `cmp_…` for a company, `per_…` for a person, and so on for every kind. A raw UUID is still accepted until 31 December 2026. An id of the wrong kind answers `400 validation_failed` instead of `404`. * **The workspace id** on `GET /account` is a `wsp_…` id derived one way from the app's id (`org_personal_…`), which it was before: it no longer matches anything the app shows. No request takes a workspace id. * **One page envelope** for every list and search: `{object: "page", data, next_cursor}`, paged with `cursor`. `starting_after`, `ending_before`, `page`, `has_more` and `next_page` are gone. * **One reference shape** for a company or person: `party`, `{object, id, name}`. * **Roles**: `/companies/{company_id}/roles` and `/persons/{person_id}/roles` replace `/companies/{id}/officers` and `/persons/{id}/positions`, with one `role` object. Every list that narrows by role takes `role_type`; the role's own field is `type`. * **Holdings of a person**: `/persons/{person_id}/holdings`, the person's shareholdings. * **Ownership tree**: `/companies/{company_id}/ownership-tree` replaces `depth` on `/shareholders`. A shareholding names its `holder` (was `owner`), and a page of shareholders carries `extract` (was `availability`). * **Events**: `/events` with `object: "event"`, a `subject`, `data` shaped by the event type, and the register entry it was read from as `source`. Its previous path, `/company-events`, keeps answering until 31 December 2026. * **Monitoring**: `/destinations` (was `/notification-endpoints`, with `address` for the address), `/deliveries`, `/event-types` and `/event-groups` (was `/monitor-event-types`). `/alerts` is gone: read what a monitor matched at `/events?monitor_id=` (free), and what was sent at `/deliveries`. A webhook body is a `delivery` holding its events. A destination has a `cadence`: instant, or a daily, weekly or monthly digest at 07:00 Europe/Berlin, and can be a Slack channel. * **Role events**: `company.officer.appointed`, `.changed` and `.removed` are `company.role.added`, `company.role.changed` and `company.role.removed`. * **Lists**: `/lists` with `list` and `list_item` (was `/collections`). * **Search** answers `summary` objects with a `type` (`company` or `person`); `city` moved to `address.city`, so a summary is a strict subset of its full record. * **The filter's role criteria** are named `roles` (were `officers`), and a condition's role type is `role_type` (was `role`). `name` matches a person's first, last or full name as well as a company holder's name, and `first_name` and `last_name` match one of a person's names. * **Errors**: a field that fails validation is always `400 validation_failed` with `errors[]`; `invalid_request` is left for a body that cannot be parsed. Renamed paths answer `410 endpoint_retired` naming the replacement. * **Credits**: `Registercheck-Credits-Charged` is on every response to a call made with a key. Prices did not change. * **`name`** is the search term on `GET /formations` and `GET /events` too; `q` keeps working on `/formations` until 31 December 2026. * **The reference** is grouped by the first segment of the path, and lists only what is served. What is planned and not served is on the [roadmap](/docs/roadmap). ## October 2026: before the one-model release [#october-2026-before-the-one-model-release] * **Search takes `name`.** `GET /search/companies`, `/search/persons` and `/search/suggestions` read the search term from `name`. `q` keeps working until 31 December 2026. * **`GET /events`** lists register events across all companies, and `GET /events/{event_id}` reads one. The previous path, `/company-events`, keeps working until 31 December 2026. * **One `status` rule.** The company record, search results, suggestions and the filter derive `status` the same way, always lowercase: `active`, `liquidation`, `terminated` or `unknown`. * **Event types renamed** (**breaking** for monitors that name them): `company.shareholder.changed` is `company.shareholding.changed`, and `company.shareholders.changed` is `company.shareholder_list.changed`. The old names answer `400` naming the new one. * **Insolvency and other register entries are monitorable**, in the groups `insolvency`, `conversion` and `other_entries`. ## 2 October 2026 [#2-october-2026] * **`legal_form`** replaces `legal_form_code` on `GET /formations`. ## 1 October 2026 [#1-october-2026] * The `/v1` naming revision: the formations feed (`/formations`) replaces `/search/new-registrations`. ## 17 September 2026 [#17-september-2026] * The `/v1` reference replaces the reference of the earlier API. # Introduction (/docs) The Registercheck API gives you programmatic access to the German commercial register — every company filed at a German *Amtsgericht*, the people who run and own them, the documents they have filed, and the changes as they happen. One HTTP call, JSON back, no PDF parsing. ```bash title="Your first request" curl "https://api.registercheck.de/v1/search/companies?name=Siemens" \ -H "Authorization: Bearer $REGISTERCHECK_API_KEY" ``` ## What you can build [#what-you-can-build] ## How the API is shaped [#how-the-api-is-shaped] The API is organised around the things the register itself contains, not around our storage, and it uses one word for each of them: in the paths, the `object` field, the ids and these pages. The [data model](/docs/concepts) explains them. The [API reference](/docs/api-reference) is grouped by the first segment of the path, so an endpoint is where its path says: `/companies/{company_id}/roles` is under Companies, `/events` under Events. | Group | What lives there | | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | [Search](/docs/api-reference/search) | Turning a name, a register number or a filter into companies and persons | | [Formations](/docs/api-reference/formations) | Companies entered in the register for the first time | | [Companies](/docs/api-reference/companies) | The register entry, and everything listed under it: roles, shareholders, holdings, documents, financial statements, its events | | [Persons](/docs/api-reference/persons) | Natural persons, the roles they hold and their shareholdings | | [Events](/docs/api-reference/events) | Every change the register courts enter, across all companies | | [Monitors](/docs/api-reference/monitors), [Destinations](/docs/api-reference/destinations), [Deliveries](/docs/api-reference/deliveries) | Watching companies, where the events go, and what was sent | | [Lists](/docs/api-reference/lists) | Your own lists of companies and persons | | [Reference data](/docs/api-reference/reference-data) | The codes other answers use, with their labels | What is planned and not served yet is on the [roadmap](/docs/roadmap). ## Before you start [#before-you-start] The register is not uniformly complete, and neither are we. What a given endpoint returns for a given company depends on what has actually been filed and extracted. [Data coverage](/docs/concepts/data-coverage) states what is populated how often, measured rather than promised. # Connect AI assistants (MCP) (/docs/mcp) Registercheck runs a remote [Model Context Protocol](https://modelcontextprotocol.io) server. An AI assistant connected to it can look up German companies in the commercial register for you, and monitor companies in your workspace for register changes. | | | | ---------- | ----------------------------------------- | | Server URL | `https://mcp.registercheck.de/mcp` | | Transport | Streamable HTTP | | Sign-in | OAuth 2.1 with your Registercheck account | ## Connect [#connect] The assistant needs to support OAuth sign-in with a Client ID Metadata Document, as ChatGPT and Claude do. Clients that can only register through Dynamic Client Registration cannot connect yet. 1. In your assistant, add a connector with the server URL above: in ChatGPT a plugin, in Claude a custom connector. In Claude you can instead add Registercheck from the [connector directory](https://claude.ai/directory/registercheck), which fills in the URL for you. 2. Sign in with your Registercheck account: with an e-mail code, with Google, or with **Mit Passwort anmelden** if your account has a password. A free account works. 3. Choose the workspace the connection acts in, then select **Erlauben**. The consent page lists everything the connection may do. To disconnect, remove the connector in your assistant. To also end the connection on our side, write to [info@registercheck.de](mailto:info@registercheck.de). ## Tools [#tools] | Tool | What it does | Changes data | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------------------- | | `search_companies` | Finds German companies by name, or by register court, register type and number | No | | `get_company` | A company's register profile: legal form, address, capital, purpose, dates, current roles | No | | `list_company_roles` | Managing directors, board members, general partners and authorized signatories (Prokura) | No | | `list_company_shareholders` | Shareholders and their shares from the latest shareholder list on file | No | | `list_company_events` | The history of register entries, newest first | No | | `list_events` | Register events across all companies, newest first, filtered by event type, role, legal form, state, court, name or date | No | | `list_company_documents` | Which register documents are on file, with their dates (not their content) | No | | `list_monitors` | The companies your workspace monitors | No | | `create_monitor` | Starts monitoring a company for register changes | Yes, adds a monitor | | `delete_monitor` | Stops monitoring a company, after the assistant asks you to confirm | Yes, removes a monitor | | `list_monitor_events` | The events your monitors matched | No | | `get_account` | Your workspace's plan and how many companies it monitors | No | Ask in plain language. For example: * "Who are the current managing directors of tado GmbH?" * "Who owns GetYourGuide Deutschland GmbH?" * "Show me the register profile of HelloFresh SE: legal form, address, share capital and purpose." * "Notify me about register changes at HelloFresh SE, then show me which companies I monitor." ## Plans and limits [#plans-and-limits] A tool uses the [credits](/docs/getting-started/credits-and-billing) of the connected workspace, exactly what the same call costs through the API: | Credits | Tools | | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Free** | `list_monitors`, `list_monitor_events`, `delete_monitor`, `get_account` | | **1** | `search_companies`, `list_company_documents` | | **10** | `get_company`, `list_company_roles`, `list_company_shareholders`, `list_company_events`, `create_monitor`, and `list_events` per page of up to 100 events | A call that fails costs nothing. Each paid tool's description says what it uses, so your AI client sees the price before it calls. Free credits do not pay for `create_monitor`. A monitor created here is like one created with an API key: it costs its 10 credits and does not count against the companies your plan monitors in the dashboard (10 on Starter, 100 on Business, 1,000 on Scale). On the free plan, monitor your one company in the dashboard. So that a chat client cannot be used to copy the register in bulk, each connection may make 60 calls a minute (with short bursts of 20) and 2,000 calls a day, and each workspace 5,000 calls a day across all its connections. A call over a limit says how long to wait. ## Data and privacy [#data-and-privacy] Answers come from Registercheck's copy of the register and name the date of the last register entry they are based on. People appear only in the context of a company, with their name, city and year of birth; the server offers no search for private persons. What we process when you use the MCP server is described in [Teil F of our Datenschutzhinweise](https://www.registercheck.de/rechtliches/datenschutz#teil-f). Questions and problems: [support@registercheck.de](mailto:support@registercheck.de). # Migration guide (/docs/migration) In October 2026 `/v1` moved to one model: one word for each thing the register holds, ids that say what they name, one page envelope for every list, and one shape for every reference to a company or person. The [glossary](/docs/concepts/glossary) has the words; this page has every change, old next to new. The changes are **breaking**, and they happen in place. The base URL stays `https://api.registercheck.de/v1`; there is no `/v2`. Three old forms are still accepted, so you can move at your own pace: * a raw UUID wherever an id is expected ([Ids](#ids)); * `q` instead of `name` on search and on `GET /formations` ([Parameters](#parameters)); * `GET /company-events` and `GET /company-events/{id}` instead of `/events` ([Paths](#paths)). A response to a request that used one carries `Deprecation` (when the replacement shipped), `Sunset: Thu, 31 Dec 2026 23:59:59 GMT` (the last moment it is served) and `Link: ; rel="deprecation"; type="text/html"` (this page). Log every response that has a `Deprecation` header and you have your to-do list. Every other renamed path answers `410 endpoint_retired` from the release on, and `detail` names its replacement. Nothing else is aliased. ## What to change [#what-to-change] **Ids.** Convert the ids you have stored to prefixed ids, or keep sending UUIDs until 31 December 2026 and convert before then. [Ids](#ids) **Paging.** Replace `starting_after`, `ending_before`, `page`, `has_more` and `next_page` with `cursor` and `next_cursor`. [Pagination](#pagination) **Paths.** Point every call at its new path. [Paths](#paths) **Fields.** Rename the fields you read, and read every reference to a company or person as a `party`. [Objects and fields](#objects-and-fields) **Webhooks.** Parse the body as a `delivery` with an `events` array. [Webhooks](#webhooks) ## Ids [#ids] Every id now starts with the kind of thing it names. The part after the `_` is the same 128-bit value as the UUID it replaces, written as a [TypeID](https://github.com/jetify-com/typeid/blob/main/spec/README.md) (version 0.3.0): 26 characters of lowercase Crockford base32. | Kind | Prefix | Kind | Prefix | | ------------------- | ------ | ----------- | ------ | | company, formation | `cmp_` | monitor | `mon_` | | person | `per_` | destination | `dst_` | | role | `rol_` | delivery | `dlv_` | | shareholding | `shr_` | list | `lst_` | | event | `evt_` | list item | `lsi_` | | register entry | `rge_` | job | `job_` | | document | `doc_` | workspace | `wsp_` | | financial statement | `fst_` | | | Workspace ids are the exception. `workspace.id` on `GET /account` was the app's own id (`org_personal_…`), not a UUID; it is now a `wsp_` id derived from it one way, so it cannot be turned back into the app's id and matches nothing the app shows. If you stored or compared it, read it again from `GET /account`. No request takes a workspace id, so nothing else changes. * **Until 31 December 2026** a raw UUID is still accepted wherever an id is expected, and the response carries `Deprecation` and `Sunset`. Responses always carry the prefixed id. * **An id of the wrong kind** — a person id where a company id belongs — answers `400 validation_failed`, and `errors[].name` names the parameter. It used to answer `404`. * Legal forms, event types, event groups and register types are codes, not ids, and did not change. The conversion is lossless and needs no call to us. Any TypeID library does it; so do these few lines, which you can run over your database once: ```python import uuid ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz" def to_typeid(prefix: str, raw: str) -> str: n = uuid.UUID(raw).int return prefix + "_" + "".join(ALPHABET[(n >> 5 * i) & 31] for i in reversed(range(26))) def to_uuid(typeid: str) -> str: n = 0 for ch in typeid.rpartition("_")[2]: n = n * 32 + ALPHABET.index(ch) return str(uuid.UUID(int=n)) to_typeid("cmp", stored_company_id) ``` ```typescript const ALPHABET = '0123456789abcdefghjkmnpqrstvwxyz'; function toTypeId(prefix: string, uuid: string): string { let n = BigInt('0x' + uuid.replaceAll('-', '')); let suffix = ''; for (let i = 0; i < 26; i++) { suffix = ALPHABET[Number(n & 31n)] + suffix; n >>= 5n; } return `${prefix}_${suffix}`; } toTypeId('cmp', storedCompanyId); ``` Both produce the spec's own test vectors. A company id and the id of its formation are the same `cmp_…` id. ## Pagination [#pagination] Every list and every search now answers one envelope, and pages forward with one cursor. | Before | Now | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `"object": "list"` (lists), `"object": "search_result"` (search) | `"object": "page"` | | `has_more: false` | `next_cursor: null` | | `starting_after=` | `cursor=` | | `page=` (search and filter) | `cursor=` | | `ending_before` (paging backwards) | gone: page forwards from the start | | `total_count` (search) | unchanged | | `availability` on a page of shareholders | `extract` (see [Shareholdings](#shareholdings)) | | the old `{ "object": "list", "has_more" }` answer, kept last by the reference-data lists (`/legal-forms`, `/courts`, `/role-types`, `/industries`, `/event-types`, `/event-groups`) | gone: they answer the page envelope above and page with `limit` and `cursor` like every other list — and with no `limit` sent, the whole list still arrives in one page, `next_cursor` `null`, as before the switch | ```python def every_item(session, url, **params): cursor = None while True: page = session.get(url, params={**params, "limit": 100, **({"cursor": cursor} if cursor else {})}).json() yield from page["data"] cursor = page["next_cursor"] if cursor is None: return ``` A cursor belongs to the list and the filters it came from. Sent with another list or other filters, it answers `400 validation_failed`. See [Pagination](/docs/getting-started/pagination). ## Paths [#paths] | Before | Now | The old path answers | | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------- | | `GET /companies/{id}/officers` | `GET /companies/{company_id}/roles` | `410` | | `GET /persons/{id}/positions` | `GET /persons/{person_id}/roles` (offices) and `GET /persons/{person_id}/holdings` (interests) | `410` | | `GET /companies/{id}/shareholders?depth=n` | `GET /companies/{company_id}/ownership-tree?depth=n`; `/shareholders` lists direct holders only | `400` for `depth` | | `GET /company-events` | `GET /events` | the old answer until 31 Dec 2026 | | `GET /company-events/{id}` | `GET /events/{event_id}` | the old answer until 31 Dec 2026 | | `GET /alerts` | `GET /events?monitor_id=` (what a monitor matched) and `GET /deliveries` (what was sent where) | `410` | | `GET /monitor-event-types` | `GET /event-types` and `GET /event-groups` | `410` | | `/notification-endpoints`, `/notification-endpoints/{id}` | `/destinations`, `/destinations/{destination_id}` | `410` | | `/collections`, `/collections/{id}` | `/lists`, `/lists/{list_id}` | `410` | | `/collections/{id}/items`, `/collections/{id}/items/{item_id}` | `/lists/{list_id}/items`, `/lists/{list_id}/items/{item_id}` | `410` | `GET /companies/{company_id}/events` stays: it is `GET /events?subject_id=` for one company. Three paths left the reference without a successor, because they were never served: `GET /companies/{id}/contact`, `GET /companies/{id}/officer-contacts` and `GET /persons/{id}/contact`. So did jobs and the by-id reads of documents and financial statements. The [roadmap](/docs/roadmap) says what is planned for each. ## Parameters [#parameters] | Where | Before | Now | | ---------------------------------------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------- | | `/search/companies`, `/search/persons`, `/search/suggestions`, `/formations` | `q` | `name` (`q` until 31 Dec 2026) | | `/company-events` → `/events` | `q` | `name` (`/company-events` keeps `q` until it goes, on 31 Dec 2026) | | every list | `starting_after`, `ending_before` | `cursor` (the old two answer `400`) | | search, `POST /search/companies/filter` | `page` | `cursor` | | `GET /companies/{company_id}` | `expand=officers` | `expand=roles` | | `GET /persons/{person_id}` | `expand=positions` | `expand=roles` | | `/events` | `officer_role` | `role_type` | | `/events` | — | `subject_id`, `monitor_id` (new; with `monitor_id` the read is free) | | `/companies/{company_id}/shareholders` | `depth` | moved to `/ownership-tree` | | `GET /companies/{id}/officers` → `GET /companies/{company_id}/roles` | `role` | `role_type` | | `GET /persons/{person_id}/roles` | — | `role_type` (new) | | `POST /search/companies/filter` | `shareholders` criteria, `owner_type` | `holder_type` (`person` or `company`, unchanged values) | | `POST /search/companies/filter` | `officers` criteria, with `role` | `roles` criteria, with `role_type` | | `POST /monitors` | `company_id` | `subject_id` (`company_id` answers `400`, with no alias) | | `POST /monitors` | `company: {register_court, register_type, register_number}` | `register: {court, type, number}` | | `POST /notification-endpoints` → `POST /destinations` | `destination` | `address` | | `POST /collections/{id}/items` → `POST /lists/{list_id}/items` | `entity: {object, id}` | `subject_id` | Sending `q` and `name` together answers `400 validation_failed` naming `q`: send one. An array parameter repeats its name: `type=company.name.changed&type=company.address.changed`. A comma-joined value is refused with `400 validation_failed`. ## Objects and fields [#objects-and-fields] ### References to a company or person [#references-to-a-company-or-person] Five shapes — `CompanyRef`, `PersonRef`, `EntityRef`, `EntityIdentifier` and the `NaturalPersonParty` / `LegalEntityParty` pair — are now one, `party`: ```json { "object": "company", "id": "cmp_01h455vb4pex5vsknk084sn02q", "name": "Beispiel Analytics GmbH" } ``` | Before | Now | | ------------------------------------------------------- | -------------------------------------------------------------- | | `type: "person"` / `type: "company"` on a party | `object` | | `person.id`, `company.id` nested in a party | `id` (`null` when the holder could not be matched to a record) | | birth date, city and country on a party | read them from `GET /persons/{person_id}` | | register number and city of an unmatched company holder | dropped | | `match_confidence` on a party | on the `role` or `shareholding` that contains it | ### Companies and search [#companies-and-search] | Object | Before | Now | | ---------- | ----------------------------------- | -------------------------------------- | | `company` | `officers` (with `expand=officers`) | `roles` | | `company` | `counts.current_officers` | `counts.current_roles` | | search hit | `object: "company_summary"` | `object: "summary"`, `type: "company"` | | search hit | `object: "person_summary"` | `object: "summary"`, `type: "person"` | | search hit | `city` | `address.city` | A summary is now a strict subset of the full record: every field it has, the record has, at the same path. ### Roles [#roles] `officer` (a company's officers) and `person_position` (a person's positions) are one object, `role`, listed from both sides. A person's shareholdings and Kommanditist interests are no longer positions: they are `shareholding` objects at `/persons/{person_id}/holdings`. | Before | Now | | ----------------------------------------------------------- | --------------------------------------------------- | | `object: "officer"`, `object: "person_position"` | `object: "role"` | | `company_id` (officer), `company` (position) | `company` (a party) | | `appointee` | `holder` (a party) | | `role` (officer), `officer_role` (position) | `type` | | `role_as_registered` | `type_as_registered` | | `type: "shareholder"`, `type: "limited_partner"` (position) | a `shareholding` at `/persons/{person_id}/holdings` | | `appointee.match_confidence` | `match_confidence` | ### Shareholdings [#shareholdings] | Before | Now | | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `owner` | `holder` (a party) | | `company` (`CompanyRef`) | `company` (a party) | | `source.document_id`, `source.document_date` | `source.id`, `source.date`, with `source.object: "document"` | | `ownership_entry` wrapping each shareholding when `depth` > 1 | `/ownership-tree`: `{object: "ownership_tree", company, nodes: [{shareholding, depth, effective_percentage, via_shareholding_id}]}` | | page field `availability` | page field `extract`: `{status, fetched_at, document_id}` | | `availability: "available"` | `extract.status: "parsed"` | | `availability: "processing"` | `extract.status: "processing"` | | `availability: "not_filed"` | `extract.status: "not_filed"` | ### Events [#events] | Before | Now | | -------------------------- | ---------------------------------------------------------------------------------------- | | `object: "company_event"` | `object: "event"` | | `company_id`, `company` | `subject` (a party) | | `previous`, `current` | `data.previous`, `data.current` for a `*.changed` type, typed like the field they change | | `officer_role`, `officers` | `data.holder` (a party) and `data.role_type` for a `company.role.*` type | | `register_entry_number` | `source`: `{object: "register_entry", id, number, published_on, text}` | `data` is shaped by `type`; [`GET /event-types`](/docs/concepts/monitoring) lists every type and its English and German label. Event types that were renamed answer `400` naming the new one, wherever you send them: a monitor's `event_types`, or the `type` filter on `/events`. | Before | Now | | ----------------------------------------------------------------------------- | ---------------------------------- | | `company.officer.appointed` | `company.role.added` | | `company.officer.changed` | `company.role.changed` | | `company.officer.removed` | `company.role.removed` | | `company.shareholder.changed` (monitor type; renamed earlier in October 2026) | `company.shareholding.changed` | | `company.shareholders.changed` (event type; renamed earlier in October 2026) | `company.shareholder_list.changed` | A `company.role.*` event names the actual role in its `data.role_type` and in its German label (*Geschäftsführer bestellt*). ### Monitors, destinations and deliveries [#monitors-destinations-and-deliveries] | Before | Now | | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `monitor.company` | `monitor.subject` (a party) | | `object: "notification_endpoint"` | `object: "destination"` | | `destination` (the address or URL) | `address` | | — | `cadence`: `instant`, `daily`, `weekly` or `monthly`; digests go out at 07:00 Europe/Berlin | | `channel`: `email` or `webhook` | `channel`: `email`, `webhook` or `slack` (new: a Slack incoming-webhook URL as `address`) | | `object: "alert"` | gone: the event is an `event` (`GET /events?monitor_id=`, free like `/alerts` was), and whether it was sent is a `delivery` (`GET /deliveries`) | | `alert.notified` | `delivery.status`: `pending`, `sent` or `failed` | | `alert.label`, `alert.label_de` | `label`, `label_de` on [`GET /event-types`](/docs/concepts/monitoring) | | `object: "monitor_event_type"`, `unavailable_groups` | `event_type` (`GET /event-types`) and `event_group` (`GET /event-groups`) | A deleted destination answers `{object: "destination", id, deleted: true}`. ### Lists [#lists] | Before | Now | | --------------------------- | --------------------- | | `object: "collection"` | `object: "list"` | | `object: "collection_item"` | `object: "list_item"` | | `collection_id` | `list_id` | | `entity` | `subject` (a party) | To add an item, send `subject_id`: a `cmp_…` or `per_…` id, whose prefix says which kind it is. Until 31 December 2026 a raw UUID is still accepted there: it is looked up as a company first, then as a person, and the response carries `Deprecation` and `Sunset`. An id of another kind (`mon_…`) answers `400 validation_failed` naming `subject_id`. Deleting a list or an item answers `{object: "list", id, deleted: true}` or `{object: "list_item", id, deleted: true}`. ### Persons [#persons] | Before | Now | | ------------------------------------- | --------------------------------- | | `positions` (with `expand=positions`) | `roles` (with `expand=roles`) | | `city`, `country` | `address.city`, `address.country` | ### Account [#account] | Before | Now | | ----------------------------------------------- | -------------------------------------------------- | | `workspace.id`: the app's id (`org_personal_…`) | `wsp_…`, derived from it one way (see [Ids](#ids)) | ## Webhooks [#webhooks] A webhook body was an `event` wrapping one `alert`. It is now a `delivery` holding the events it sends: one for an instant destination, several for a daily, weekly or monthly digest. One parser serves both. ```json { "object": "delivery", "id": "dlv_01h455vb4pex5vsknk084sn02q", "destination_id": "dst_01h455vb4pex5vsknk084sn02r", "cadence": "instant", "events": [ { "object": "event", "id": "evt_01h455vb4pex5vsknk084sn02s", "type": "company.registered_office.changed", "subject": { "object": "company", "id": "cmp_01h455vb4pex5vsknk084sn02t", "name": "Beispiel Analytics GmbH" }, "data": { "previous": "Strehla", "current": "Roskow" }, "effective_date": "2026-09-21" } ], "created_at": "2026-10-20T13:20:17Z" } ``` | Before | Now | | ----------------------------------- | ------------------------------------------------------------------------ | | `object: "event"` (the envelope) | `object: "delivery"` | | `id` (the envelope; also `svix-id`) | `id`, a `dlv_…` id; still the `svix-id`, still the key to deduplicate on | | `type` | each event's `type` | | `data.object` (one alert) | `events` (one or more events) | | `data.object.monitor_id` | the events a monitor matched: `GET /events?monitor_id=` | Signing is unchanged: the same headers and the same secret. A retry that was already queued at the release goes out in the new shape. See [Webhooks](/docs/concepts/webhooks). ## Errors [#errors] | Before | Now | | ------------------------------------------------------- | ------------------------------------------------------------------------------------ | | `400 invalid_request` for a field that fails validation | `400 validation_failed`, with `errors: [{location, name, detail}]` naming each field | | `400 invalid_request` | only for a body that cannot be parsed at all | | `404` for an id of the wrong kind | `400 validation_failed` | | a renamed path: `404` | `410 endpoint_retired`, `detail` naming the new path | See [Errors](/docs/getting-started/errors). ## Credits [#credits] Prices did not change, and reading what your own monitor matched (`GET /events?monitor_id=`) stays free, as `/alerts` was. `Registercheck-Credits-Charged` is now on every response to a call with a key, `0` when nothing was charged; `Registercheck-Credits-Remaining` comes whenever the call read your balance. See [Credits and billing](/docs/getting-started/credits-and-billing). # Roadmap (/docs/roadmap) The [API reference](/docs/api-reference) lists only what is served: every operation in it answers today. The operations below are designed and not served yet. A call to any of them answers `404`, and none of them is charged. ## Contact details [#contact-details] | Planned | What it returns | | ------------------------------------- | --------------------------------------------------------------------------------------- | | `GET /companies/{company_id}/contact` | A company's website, e-mail address, phone number, social profiles and Crunchbase entry | | `GET /persons/{person_id}/contact` | A person's e-mail addresses, phone number, social profiles and Crunchbase entry | A third read will return the contact details of everyone who holds a current role at a company, in one call. Links come in two objects: `links` for the links that belong to the record — its own website, its page on registercheck.de, its Crunchbase entry — and `socials` for the profiles on social networks. A link that was not found is `null`. The company's answer, shortened: ```json { "links": { "website": "https://example.com", "registercheck": "https://www.registercheck.de/companies/cmp_0bdw55570y9hart3hzdwnrm727", "crunchbase": "https://www.crunchbase.com/organization/example" }, "socials": { "linkedin": "https://www.linkedin.com/company/example", "x": null, "facebook": null, "instagram": null } } ``` Contact details are not register data: they are gathered from public web sources and go out of date on their own schedule. They will be an add-on beside the register data, priced apart, and every answer will say when it was gathered. **Not built yet.** Until then, the register gives you the registered office on the company record ([`GET /companies/{company_id}`](/docs/concepts/companies)). ## Jobs: fetching data again [#jobs-fetching-data-again] | Planned | What it does | | -------------------- | -------------------------------------------------------------------------------------------------------------- | | `POST /jobs` | Ask for a company's documents, financial statements or ownership history to be fetched from the register again | | `GET /jobs/{job_id}` | Poll that work until it has `succeeded` or `failed` | Some work cannot happen inside a request, so it will be a job you start and then poll. **Not served, for two reasons.** No worker consumes the queue yet, so a job would be accepted and never run. And a stored job records neither its company nor its type, so it could not describe itself back to you. Offering the endpoint before both are fixed would sell a request that does nothing. What to do instead: * **Shareholders fetch themselves.** When we hold no shareholder list for a company, [`GET /companies/{company_id}/shareholders`](/docs/concepts/ownership) starts reading one. The page then says `extract.status: "processing"`, and the same request a little later returns the holders. One call starts at most one extraction, for the company you asked about. * **Documents are listed as they are on file.** `GET /companies/{company_id}/documents` lists what we hold. If the document you need is not there, a job would not conjure it: for many entries nothing was ever filed, or the entry has been deleted. See [Data coverage](/docs/concepts/data-coverage). ## Documents and financial statements by id [#documents-and-financial-statements-by-id] | Planned | What it returns | | ------------------------------------------------------------ | --------------------------------------------------------- | | `GET /documents/{document_id}/content` | The document file, through a short-lived download link | | `GET /financial-statements/{financial_statement_id}` | One financial statement with its figures | | `GET /financial-statements/{financial_statement_id}/content` | The statement's file, through a short-lived download link | **Not served, because our data store cannot yet look a document or a statement up by its id.** Each is reachable through its company instead: * `GET /companies/{company_id}/documents` lists a company's documents. * `GET /companies/{company_id}/financial-statements` lists its statements, each one with its figures — the same object the by-id read will return. Until the by-id reads are served, `links.download` on a document or statement is `null`. # Support (/docs/support) ## Getting in touch [#getting-in-touch] | | | | --------------------------------------------- | ----------------------------------------------------------- | | Sales, trials, raising your request allowance | [sales@registercheck.de](mailto:sales@registercheck.de) | | Technical problems | [support@registercheck.de](mailto:support@registercheck.de) | | Shared Slack Connect channel | Ask your account contact | ## Before you write [#before-you-write] Most questions are answered faster by one of these: * A `429` on a key that worked yesterday is usually the [monthly request allowance](/docs/getting-started/rate-limits), not the key. * An empty list of shareholders, roles or financial statements is usually [coverage](/docs/concepts/data-coverage), not a bug. * A `500` from monitoring is usually a duplicate monitor or an unknown `event_types` value — see [Monitoring](/docs/concepts/monitoring). ## What to include [#what-to-include] For anything we have to reproduce: * The **full request URL**, with parameters, and the method. * The **response status** and body. * The **company or person id**, if the question is about specific data. * A **timestamp with a timezone**, so we can find the request in our logs. Please do not send us your API key. If you believe a key has leaked, revoke it on the [Usage (API & Nutzung)](https://registercheck.de/api-management) page and tell us afterwards. ## Reporting wrong data [#reporting-wrong-data] Register data can be wrong for two different reasons, and it helps to say which you think it is: 1. **We disagree with the register.** Tell us the company id and what the register says. These are extraction faults and we can fix them. 2. **The register itself is wrong or out of date.** We cannot correct the register. The company must file a correction with its *Amtsgericht*; once filed, it reaches us on the normal cycle. ## Removing personal data [#removing-personal-data] Natural persons named in the register can object to their data being published here. Requests go to [datenschutz@registercheck.de](mailto:datenschutz@registercheck.de) and are handled under the GDPR. A suppressed person's records stop being served through the API; you are responsible for removing any copy you have already stored. # API reference (/docs/api-reference) Every request goes to `https://api.registercheck.de/v1` and carries your API key as `Authorization: Bearer `; see [Authentication](/docs/getting-started/authentication). Each endpoint's page states what it costs. The reference is grouped by the first segment of the path, and lists what is served; what is planned is on the [roadmap](/docs/roadmap). | Group | What it covers | Endpoints | | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | | [Search](/docs/api-reference/search) | Turning a name, a register number or a set of filters into companies and persons. | 4 | | [Formations](/docs/api-reference/formations) | Companies whose first register entry is a new registration, newest register day first. | 3 | | [Companies](/docs/api-reference/companies) | The register entry itself — name, registered office, purpose, capital, legal form — and everything listed under it: roles, shareholders, holdings, documents, financial statements and the events of one company. | 10 | | [Persons](/docs/api-reference/persons) | Natural persons named in the register, the roles they hold and the shareholdings they hold, across every company. | 3 | | [Events](/docs/api-reference/events) | Every change the register courts enter, across all companies, newest register day first. | 4 | | [Monitors](/docs/api-reference/monitors) | Watching a company for the event types you choose. | 5 | | [Destinations](/docs/api-reference/destinations) | Where your monitors' events are sent: e-mail addresses and webhook URLs. | 5 | | [Deliveries](/docs/api-reference/deliveries) | What was sent to which destination, and the webhook a delivery arrives as. | 3 | | [Lists](/docs/api-reference/lists) | Your own lists of companies and persons. | 9 | | [Reference data](/docs/api-reference/reference-data) | The codes other responses use — legal forms, courts, role types, industries, event types and event groups — with their labels. | 6 | | [Account](/docs/api-reference/account) | The workspace your key acts in, its plan, the credits it has left and your monitor usage. | 1 | | [Platform](/docs/api-reference/platform) | Service health. | 1 | # Build a company report (/docs/guides/build-a-company-report) A full report is four calls on top of the company record. Fire them concurrently — they do not depend on each other. ```python import os, requests from concurrent.futures import ThreadPoolExecutor BASE = "https://api.registercheck.de/v1" session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['REGISTERCHECK_API_KEY']}" def report(company_id: str) -> dict: parts = { "company": f"/companies/{company_id}", "roles": f"/companies/{company_id}/roles", "shareholders": f"/companies/{company_id}/shareholders", "history": f"/companies/{company_id}/events", } def get(path): r = session.get(BASE + path, timeout=60) return r.json() if r.ok else None with ThreadPoolExecutor(max_workers=4) as pool: results = dict(zip(parts, pool.map(get, parts.values()))) company = results["company"] or {} # Financials cost 10 credits whether or not anything is filed - check first. if (company.get("counts") or {}).get("financial_statements"): results["financials"] = get(f"/companies/{company_id}/financial-statements") return results ``` That is 40 credits, or 50 with financials. `GET /companies/{company_id}/kyb-report` is the same register entry, current roles, shareholders and ownership tree to depth 3 in one response, for 50 credits. It does not include the event history or the financial statements, and it returns document references rather than the files. A `next_cursor` on its roles or shareholders page is read from those lists. The report is computed for that request. It is not an extract from the Transparenzregister, and it is no substitute for your own due diligence. ## Cost control [#cost-control] Check before you fetch. The company record already tells you whether there is anything to find: | Field on the company record | Skip the call when | | ----------------------------- | --------------------------------------------- | | `counts.financial_statements` | `0` — nothing has been filed | | `counts.documents` | `0` — no document is on file to download | | `counts.current_roles` | `0` — there is no current role holder to list | That second row is worth taking: `representation` on the company record covers most of what `/management/contact` returns, and the record costs 10 credits against the contact endpoint's 25. ## Presenting it honestly [#presenting-it-honestly] Three things will make your report defensible, and their absence will make it wrong: Show `source.date` next to shareholdings, `period.fiscal_year` next to financial figures, and `last_register_entry_date` next to the register entry. A cap table with no as-of date is a claim you cannot support when challenged. **Distinguish "no data" from "no such thing".** An empty `shareholders` array on an `AG` means AGs do not file shareholder lists. The same empty array on a `GmbH` whose `address` and `purpose` are also null means no extract has been parsed. Render those two differently or your users will draw the wrong conclusion. **Show `liquidation` as its own state.** A company in liquidation still has a live register entry, but it is being wound up; folding it into `active` hides exactly what a counterparty check looks for. `unknown` means the register data does not say, not "inactive" — see [Companies](/docs/concepts/companies). ## Keeping it current [#keeping-it-current] Do not re-run the report on a schedule. Create a [monitor](/docs/guides/track-changes) and rebuild only when an event says something moved. # Find a company (/docs/guides/find-a-company) Almost every integration starts here: you have a string, and you need the right company. There are three ways in, and they are not interchangeable. ## If you have a register number [#if-you-have-a-register-number] This is the only exact route. Give the number and the court's code (`D2505` is Amtsgericht Memmingen): ```bash curl -G "https://api.registercheck.de/v1/search/companies" \ --data-urlencode "register_number=13557" \ --data-urlencode "register_court=D2505" \ -H "Authorization: Bearer $REGISTERCHECK_API_KEY" ``` Register numbers repeat across courts — HRB 670 exists at most of them — so a number without a court will match many unrelated companies. ## If a human is typing [#if-a-human-is-typing] Use [`/search/suggestions`](/docs/api-reference/search/listSuggestions). It is built for autocomplete: one call per keystroke-ish, cheap, and ranked. ```typescript const res = await fetch( `${BASE}/search/suggestions?name=${encodeURIComponent(term)}&limit=8`, { headers: { Authorization: `Bearer ${key}` } }, ); const { data } = await res.json(); ``` Each hit is a `summary`, and its `type` says whether it is a `company` or a `person`. For a company, show `register.formatted` and `status` next to the name. Two companies in the same town with nearly the same name are common, and the register line is how your user tells them apart. Debounce to \~250 ms, and start at the second character: a term shorter than 2 characters (or longer than 200) is a `400` and costs nothing. At 1 credit a call, a busy search box is the one place this API can quietly get expensive. ## If you are matching a list [#if-you-are-matching-a-list] Use [`/search/companies`](/docs/api-reference/search/searchCompanies) with as many fields as you have. They combine, and `name` or `register_number` is required: | Parameter | Matching | | ----------------------------------------------------------------------- | -------------------------------------------- | | `name` | the name or part of it, by words (see below) | | `register_number`, `register_court` (the court's code), `register_type` | exact | | `city` | exact, the city of the registered address | | `status` | any of the statuses given | A parameter the endpoint does not read answers `400 validation_failed`, naming it in `errors[].name`, and costs nothing. `?name=Siemens®ister_court_code=D2601` (the name before 2026-10-02) is refused rather than answered with Siemens companies at every court. ### How `name` matches [#how-name-matches] `name` matches by words, ignoring case, on all three searches: * **Every word must appear**, in any order: `name=Enduro Reise` finds *Enduro Reise GmbH* and *Reise Enduro UG*. * **The last word also matches as the start of a word**: `name=Beisp` finds *Beispiel GmbH*. * **Small typos are tolerated**: `name=Siemsn` finds *Siemens*. * **A fragment from the middle of a word does not match**: `name=analytics` does not find *Dataanalytics GmbH*. * **When no name contains every word**, names that contain fewer of them are returned instead, so check the hits rather than assuming each one carries every word. Company search ranks active companies first, then the best match; person search and suggestions rank by match. Until 2026-12-31 the searches still accept `q` and treat it exactly like `name`. Every response to a request that uses it carries `Deprecation` ([RFC 9745](https://www.rfc-editor.org/rfc/rfc9745)) and `Sunset: Thu, 31 Dec 2026 23:59:59 GMT` ([RFC 8594](https://www.rfc-editor.org/rfc/rfc8594)) headers, and a `Link` with `rel="deprecation"` to this section. After that date `q` is removed, and a request that uses it is refused like any unknown parameter. Sending both `name` and `q` is a `400`. ### Matching well [#matching-well] * **Strip the legal form before comparing.** `legal_form` tells you which one the name carries. * **Use the city.** `name` plus `city` resolves most ambiguity in one call. * **Expect branches.** A name that matches several entries at different courts is usually one business with registered branches, not a bad match. * **Score, do not guess.** `total_count` of 1 is a match; `total_count` of 40 means you need another field. ## Find a person's companies [#find-a-persons-companies] The routes above start from a company's name or number, and `/search/companies` takes no person. When you start from a person, use [`POST /search/companies/filter`](/docs/api-reference/search/filterCompanies), at 25 credits a page, with a condition on the company's `roles`. ```bash curl -X POST "https://api.registercheck.de/v1/search/companies/filter" \ -H "Authorization: Bearer $REGISTERCHECK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "roles": { "conditions": [{ "person_id": "per_3v3w754f3t9w79tajtdgf8pkcg" }] }, "company": { "status": ["active"] }, "limit": 20 }' ``` The `person_id` is the `id` of a hit from [`GET /search/persons`](/docs/api-reference/search/searchPersons), at 1 credit a call. A condition can also ask by name, as a substring: `{"name": {"contains": "Hans Müller"}}` matches a person's first, last or full name as well as the name of a company that holds a role, and `first_name` and `last_name` match one of a person's names. Only `contains` is applied to these three (`equals` and `starts_with` answer `400`), and a substring also matches longer names (`Müller` finds `Müllermann`), so the id is the precise way when you have one. The answer is a page of `summary` hits, as from `/search/companies`: here, the active companies where that person holds or held a role, as a managing director, a board member, a liquidator and the like. Page through it with `cursor`: send the same filter again, with `cursor` set to the page's `next_cursor`. A cursor holds its filter: sent with a changed filter, it answers `400 validation_failed` naming `/cursor`. These limits apply today: * **One `person_id` per condition, and one `roles` condition per filter.** A second condition, or `min_matching` / `max_matching` even with one condition, answers `400`: the data store does not combine conditions yet. One human who appears under two person ids takes two filters, 25 credits each. * **`role_type` narrows to Prokura only.** Prokura is held apart from the other roles: without `role_type` the condition reads a person's other roles, and `"role_type": ["authorized_signatory"]` reads the companies where they hold Prokura instead. Any other role type answers `400`: the register data records no role type for the other roles. Prokura holders are found by `person_id`, `status` and the date fields, not by name: `name`, `first_name` or `last_name` together with `"role_type": ["authorized_signatory"]` answers `400`. If one person's roles are all you need, [`GET /persons/{person_id}/roles`](/docs/api-reference/persons/listPersonRoles) lists them for 10 credits. [Filtering](/docs/concepts/filtering) has the other conditions, and [Persons](/docs/concepts/people) explains why one human can appear under several ids. ## Then store the id [#then-store-the-id] ```json { "id": "cmp_63b6n0c6p09pnsrr6nrtd0dzsc" } ``` Store that `id`. Names change, companies are renamed and re-registered, and register numbers are reused across courts. The `id` is the only stable handle. # Follow new formations (/docs/guides/follow-new-formations) ## The shape of it [#the-shape-of-it] ```text every few minutes → GET /formations?created_at[gte]= → page to the end → dedupe by id → T = newest created_at ``` A formation is a company whose first register entry is a new registration, not a move of the registered office, a conversion or a merger. The list is ordered by the Registertag (`registration_date`), newest first. That is the order you want to read, and the wrong one to poll on. ## Why not poll on the Registertag [#why-not-poll-on-the-registertag] * **Late finds.** About a quarter of formations are found days after their Registertag. They sort into the middle of the list, below rows you already read, so paging from the top stops before it reaches them. * **Visible after discovery.** A formation is usually published a few minutes after we find the company: its first register entry is written about two minutes later, and the list refreshes within a minute of that write. When the write takes longer, the formation appears later. `created_at` is the moment a formation became available through the API. Every formation you have not seen yet has a `created_at` later than your last poll, whatever its Registertag. ## Poll [#poll] ```python from datetime import datetime, timedelta, timezone OVERLAP = timedelta(minutes=10) def poll(since: datetime, seen: set[str]) -> datetime: newest = since params = {"created_at[gte]": (since - OVERLAP).strftime("%Y-%m-%dT%H:%M:%SZ"), "limit": 100} while True: r = session.get(f"{BASE}/formations", params=params) r.raise_for_status() page = r.json() for formation in page["data"]: if formation["id"] in seen: # the overlap re-reads a few; that is its job continue seen.add(formation["id"]) handle(formation) created = datetime.fromisoformat(formation["created_at"].replace("Z", "+00:00")) newest = max(newest, created) if page["next_cursor"] is None: return newest # store it; it is the next `since` params["cursor"] = page["next_cursor"] ``` Three rules: 1. **Overlap by ten minutes and deduplicate by `id`.** Delivery is at-least-once and eventually consistent: a formation usually appears about three minutes after we find the company, sometimes later, and its `created_at` is the moment it appeared, so the overlap catches it. Keep the ids of the last hour or so; that is enough. 2. **Page forward with `cursor`** until `next_cursor` is `null`. The order is stable (`registration_date`, then the order we found them in, then `id`, all descending), so a row that arrives while you page is never served twice in one walk and is caught by the next poll. 3. **Every page costs 10 credits.** Poll every few minutes, not every few seconds, and keep `limit=100`: a quiet interval is one page. Filters combine with the poll. `legal_form=gmbh&legal_form=ug&state=DE-BY` follows new GmbHs and UGs in Bavaria; `capital[gte]=25000` only the ones with at least 25,000 EUR of capital. [`GET /legal-forms`](/docs/api-reference/reference-data/listLegalForms) lists every `legal_form`. ```bash curl -G -g https://api.registercheck.de/v1/formations \ -H "Authorization: Bearer $REGISTERCHECK_API_KEY" \ -d "created_at[gte]=2026-09-30T14:00:00Z" \ -d "legal_form=gmbh" \ -d "legal_form=ug" \ -d "state=DE-BY" \ -d "capital[gte]=25000" \ -d "limit=100" ``` `-g` stops curl from reading the square brackets as a glob pattern. To look back over a range of Registertage instead, bound `registration_date[gte]` and `registration_date[lte]`. ## Keep your own notes on a formation [#keep-your-own-notes-on-a-formation] ```python session.patch(f"{BASE}/formations/{formation_id}", json={"metadata": {"crm_id": "0015g00000XyZ", "stage": "contacted"}}) ``` Metadata merges: a key you set is added or replaced, a key set to `null` is removed, and a key you do not name is left alone. It belongs to your workspace; nobody else sees it. A formation stays retrievable with its metadata after it leaves the list's 180-day window. Each update costs 1 credit and returns the formation. ## What to know [#what-to-know] * **The window.** The list covers the last 180 days of Registertage. Without a `registration_date` or `created_at` bound it shows the last 30. * **Completeness.** Registertage before 2026-09-09 are incomplete until they are back-filled. * **Right after a restart** the list can answer `503` with `Retry-After`. Wait that many seconds and poll again with the same `since`; nothing was charged. # Follow register changes (/docs/guides/follow-register-changes) ## The shape of it [#the-shape-of-it] ```text every few minutes → GET /events?created_at[gte]= → page to the end → dedupe by id → T = newest created_at ``` `GET /events` lists what the register courts entered for any company: a new managing director, a moved registered office, a changed purpose, a capital increase, an insolvency, a deletion. Each item is an `event`, the same object [`GET /companies/{company_id}/events`](/docs/api-reference/companies/listCompanyEvents) lists for one company, with the company named in `subject`, so you need no extra call to know whose it is. A company's first entry, its registration, is not in this list. New companies are [`GET /formations`](/docs/guides/follow-new-formations). `GET /company-events` is the previous name of this list. It keeps answering exactly as `GET /events` does, at the same price, until 31 December 2026, and is deprecated: its answers carry `Deprecation`, `Sunset` and `Link` headers, and a new integration should call `GET /events`. ## Events, not entries [#events-not-entries] One register entry can carry several events. An entry that removes one managing director and appoints another is two `company.role.*` events with the same register entry as their `source`, and `data.role_type` tells you the role (`managing_director`, `authorized_signatory` for a Prokura, `liquidator`, …). Group by `source.id` if you want one row per entry. The `type` says what the entry did, read from its text: a deletion is `company.deregistered`, a change to the articles of association is `company.articles.changed`, insolvency proceedings are `company.insolvency.recorded`. An entry that only repeats the registered office or the name already on file is not published. ## Why not poll on `effective_date` [#why-not-poll-on-effective_date] The list is ordered by the register day (`effective_date`), newest first. That is the order you want to read, and the wrong one to poll on: * **Late arrivals.** About 7 % of entries reach Registercheck one or more days after their register day. They sort into the middle of the list, below rows you already read, so paging from the top stops before it reaches them. * **The register records a day, not a time.** Two polls on the same `effective_date` cannot tell what is new. `created_at` is the moment an event became available through the API. Every event you have not seen yet has a `created_at` later than your last poll, whatever its register day. It moves only when an event is served again: the register rewrote the entry (its type, day, entry number or the entry before it), and the event comes back under the same `id` with the new values. An event that no longer stands for a change (it now restates an earlier entry, or its day left the 180-day window) is no longer served; `GET /events/{id}` answers 404 for it. ## Poll [#poll] ```python from datetime import datetime, timedelta, timezone OVERLAP = timedelta(minutes=10) def poll(since: datetime, seen: dict[str, str]) -> datetime: newest = since params = {"created_at[gte]": (since - OVERLAP).strftime("%Y-%m-%dT%H:%M:%SZ"), "limit": 100} while True: r = session.get(f"{BASE}/events", params=params) r.raise_for_status() page = r.json() for event in page["data"]: if seen.get(event["id"]) == event["created_at"]: continue # the overlap re-reads a few; that is its job seen[event["id"]] = event["created_at"] handle(event) # new, or an update of an event you have created = datetime.fromisoformat(event["created_at"].replace("Z", "+00:00")) newest = max(newest, created) if page["next_cursor"] is None: return newest # store it; it is the next `since` params["cursor"] = page["next_cursor"] ``` With `curl`, the brackets need `-g` so they are not read as a glob: ```bash curl -G -g https://api.registercheck.de/v1/events \ -H "Authorization: Bearer $REGISTERCHECK_API_KEY" \ -d "created_at[gte]=2026-09-30T14:00:00Z" \ -d "type=company.role.added" \ -d "role_type=managing_director" \ -d "state=DE-BY" \ -d "limit=100" ``` Three rules: 1. **Overlap by ten minutes and deduplicate by `id` and `created_at`.** Delivery is at-least-once: the overlap re-reads events you already have, which is what the deduplication is for. An `id` you have with a newer `created_at` is an update: replace what you stored. Keep the last hour or so of pairs; that is enough. 2. **Page forward with `cursor`**, sending each page's `next_cursor`, until it is `null`. The order is stable (`effective_date`, then `created_at`, then `id`, all descending), so an event that arrives while you page is never served twice in one walk and is caught by the next poll. 3. **Every page costs 10 credits.** Poll every few minutes, not every few seconds, and keep `limit=100`: a quiet interval is one page. Retrieving one event by id costs 1 credit. ## Narrow it [#narrow-it] Filters combine with the poll (AND across parameters, OR within a repeated one): | You want | Add | | --------------------------------------------------- | ------------------------------------------------------------------------------- | | New managing directors | `type=company.role.added&role_type=managing_director` | | Insolvencies and deletions | `type=company.insolvency.recorded&type=company.deregistered` | | GmbHs and UGs in Bavaria | `legal_form=gmbh&legal_form=ug&state=DE-BY` | | One court | `register_court=D2601`, or its name (`Amtsgericht München`) or seat (`München`) | | Companies with at least 25,000 EUR capital | `capital[gte]=25000` | | Words in the name, the seat, the court or the entry | `name=holding augsburg` | `name` does not search people's names. ## When a cursor stops working [#when-a-cursor-stops-working] An event whose company or person has been restricted since you listed it is no longer served, and a `cursor` positioned on it answers `400 validation_failed`. Do not retry the cursor: run the poll again from your stored `since`. That is the same recipe, and it picks up exactly where you were. ## What to know [#what-to-know] * **The window.** The list covers the last 180 days of register days. Without any `effective_date[*]` or `created_at[*]` bound it shows the last 30. An `effective_date[gte]` older than 180 days answers `400 validation_failed` naming the parameter. * **Completeness.** Register days before 2026-09-10 are incomplete. * **Where it comes from.** Registercheck reads each court's register continuously; most entries are here on their own register day, from about 07:00. * **Right after a restart** the list can answer `503` with `Retry-After`. Wait that many seconds and poll again with the same `since`; nothing was charged. * **One company, pushed to you.** To be told about one company's changes without polling, use a [monitor and a webhook](/docs/guides/track-changes). # Track changes (/docs/guides/track-changes) ## The shape of it [#the-shape-of-it] ```text monitor once → we re-read the register → event fires → your webhook → you re-read the company ``` You monitor a company. We detect that its register entry moved. Your destination gets told. You fetch the new state. A monitor follows the companies you name. To follow changes across every company -- all new managing directors in Bavaria, every insolvency -- poll the register-event feed instead: [Follow register changes](/docs/guides/follow-register-changes). ## Monitor a company [#monitor-a-company] ```python import uuid def watch(court: str, register_type: str, number: str, events=("*",), crm_id: str | None = None) -> str: r = session.post( f"{BASE}/monitors", headers={"Idempotency-Key": str(uuid.uuid4())}, # reuse it if you retry this call json={ "register": {"court": court, "type": register_type, "number": number}, "event_types": list(events), "metadata": {"crm_id": crm_id} if crm_id else {}, }, ) if r.status_code == 409 and r.json()["code"] == "conflict": return r.json()["existing_id"] # already watching it r.raise_for_status() return r.json()["id"] watch("München", "HRB", "999999", events=("representation", "capital"), crm_id="0015g00000XyZ") ``` No search first: the register identity names the company. No list-then-check either: a second monitor on the same company answers `409` with the existing monitor's id, and the `Idempotency-Key` makes a retry after a timeout safe — creating a monitor costs 10 credits. ## Narrow the events [#narrow-the-events] `*` is the default and it is noisy. Monitor what you will act on: | You care about | Monitor | | ------------------------- | ------------------------------------------------------------------------------------------------------ | | A counterparty's solvency | `insolvency`, `dissolution`, `capital` | | Who can sign | `representation` | | Change of control | `shareholders` — accepted, but not delivered yet: the monitor lists these in `unavailable_event_types` | | Keeping records tidy | `identity`, `registered_office`, `conversion` | "Who can sign" is one set of types: an authorised signatory holds a role here, so a grant of *Prokura* and a board appointment both raise `company.role.added`, and `data.role_type` says which. One list names every event type ([`GET /event-types`](/docs/api-reference/reference-data/listEventTypes)); `monitorable` says which a monitor can ask for. A register entry keeps its name in the history and on a monitor, and four types are monitoring's own. Shareholders are reported one by one (`company.shareholder.added`, `company.shareholding.changed`, `company.shareholder.removed`), where the history records a newly filed shareholder list once (`company.shareholder_list.changed`). And `company.financial_statement.published` comes from the Bundesanzeiger and has no counterpart in the register history. Change the selection later with [`PATCH /monitors/{id}`](/docs/api-reference/monitors/updateMonitor), which touches only what you send — so `{"status": "paused"}` stops delivery without losing your event types. ## Handle the webhook [#handle-the-webhook] ```typescript app.post('/hooks/registercheck', async (req, res) => { const delivery = verify(req); // see Webhooks: raw body + the three svix-* headers res.status(200).end(); // acknowledge first, work after if (await alreadyProcessed(delivery.id)) return; // delivery.id is the svix-id; a retry repeats it for (const event of delivery.events) { // one for an instant destination, a period's for a digest const fresh = await fetchCompany(event.subject.id); // the payload is a signal, not state await applyChange(event.subject.id, event.type, fresh); } await markProcessed(delivery.id); }); ``` Three rules, in order of how much trouble they save you: 1. **Acknowledge immediately.** A slow handler is a retried handler. 2. **Deduplicate on the delivery `id`** (also the `svix-id` header). Assume at-least-once delivery; a retry repeats it. See [Webhooks](/docs/concepts/webhooks). 3. **Re-read the company.** Treat the event as "something changed", not as the new value. ## What this is not [#what-this-is-not] Detection runs on a cycle against the register, and the register itself lags the real world by days or weeks. Expect to hear about a change a day or two after it is published, and do not build anything that needs to know within the hour. ## Housekeeping [#housekeeping] Only creating a monitor costs anything; listing, reading, updating and deleting are free. So read `GET /monitors` whenever you want the truth rather than trusting a local index, and delete what you no longer watch — the cost of a monitor is the 10 credits you already paid, not an ongoing charge. # Authentication (/docs/getting-started/authentication) Every request except `GET /health` needs an API key, sent as a bearer token: ```http Authorization: Bearer rc_live_YOUR_KEY ``` There is no other authentication scheme. Query-string keys, cookies and the `X-API-Key` header are not accepted on the customer API. ## Creating and revoking keys [#creating-and-revoking-keys] Keys are managed on the [Usage (API & Nutzung)](https://registercheck.de/api-management) page. A key is displayed once, at creation. Losing it means revoking it and creating another. Revocation takes effect immediately, on the next request. ## What a key is scoped to [#what-a-key-is-scoped-to] A key belongs to **your account**, not to a project or an environment. Every key you hold reads and writes the same lists and the same monitors, and draws on the same credit balance. Use separate names to tell them apart in the usage breakdown; do not expect them to be isolated from one another. Besides each key's own monthly allowance, the same allowance is enforced across your account. Creating a second key does not give you a second allowance — see [Rate limits](/docs/getting-started/rate-limits). ## Checking a key works [#checking-a-key-works] `GET /health` needs no key and tells you the API is reachable. To check the key itself, use the cheapest authenticated call: ```bash curl -sS -o /dev/null -w '%{http_code}\n' \ "https://api.registercheck.de/v1/search/suggestions?name=test&limit=1" \ -H "Authorization: Bearer $REGISTERCHECK_API_KEY" ``` `200` means the key is good. For anything else, see below. ## Authentication failures [#authentication-failures] | Status | Body | What it means | | ------ | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `400` | `Authorization header missing or invalid` | No `Authorization` header, or it does not start with `Bearer `. | | `401` | `UNAUTHORIZED` | The key is wrong, revoked, disabled or expired. | | `402` | `INSUFFICIENT_CREDITS` | The key is valid; the account has no credits left for this call. | | `429` | `RATE_LIMITED` | The key is valid; it or your account has used its [request allowance](/docs/getting-started/rate-limits). Wait for `Retry-After`. | | `503` | `SERVICE_UNAVAILABLE` | Keys cannot be checked right now; your key was **not** rejected. Retry after `Retry-After`; do not rotate it. | ## Keeping keys safe [#keeping-keys-safe] * Keys carry the full permissions of your account, including deleting lists and monitors. Treat one as a password. * Never ship a key in browser code. Every call in these docs is a server-side call. * Rotate by creating the new key, deploying it, then revoking the old one — in that order. # Credits and billing (/docs/getting-started/credits-and-billing) The API is prepaid. You hold a balance of **credits**; each successful call deducts the credits that call costs. Your plan adds its credits every month: 1,000 on Starter, 10,000 on Business and 100,000 on Scale. A call through the [MCP server](/docs/mcp) costs exactly what it costs here. Credits are bought on the [Usage (API & Nutzung)](https://registercheck.de/api-management) page. ## What each call costs [#what-each-call-costs] Reading and managing your monitors, destinations, deliveries and lists costs nothing, and so does reference data. Only reading register data, and creating a monitor, is charged. Every operation's page in the API reference also states its price. | Cost | Operations | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Free** | `GET /monitors`, `GET /monitors/{monitor_id}`, `PATCH /monitors/{monitor_id}`, `DELETE /monitors/{monitor_id}`, `GET /destinations`, `POST /destinations`, `GET /destinations/{destination_id}`, `PATCH /destinations/{destination_id}`, `DELETE /destinations/{destination_id}`, `GET /deliveries`, `GET /deliveries/{delivery_id}`, `GET /lists`, `POST /lists`, `GET /lists/{list_id}`, `PATCH /lists/{list_id}`, `DELETE /lists/{list_id}`, `GET /lists/{list_id}/items`, `POST /lists/{list_id}/items`, `PATCH /lists/{list_id}/items/{item_id}`, `DELETE /lists/{list_id}/items/{item_id}`, `GET /event-types`, `GET /event-groups`, `GET /legal-forms`, `GET /courts`, `GET /role-types`, `GET /industries`, `GET /account`, `GET /health` | | **1 credit** | `GET /search/companies`, `GET /search/persons`, `GET /search/suggestions`, `GET /companies/{company_id}/documents`, `GET /events/{event_id}`, `GET /formations/{formation_id}`, `PATCH /formations/{formation_id}` | | **10 credits** | `GET /companies/{company_id}`, `GET /companies/{company_id}/roles`, `GET /companies/{company_id}/shareholders`, `GET /companies/{company_id}/holdings`, `GET /companies/{company_id}/ownership-tree`, `GET /companies/{company_id}/financial-statements`, `GET /companies/{company_id}/events`, `GET /persons/{person_id}`, `GET /persons/{person_id}/roles`, `GET /persons/{person_id}/holdings`, `GET /events`, `GET /formations`, `POST /monitors` | | **25 credits** | `POST /search/companies/filter`, `GET /companies/{company_id}/beneficial-owners` | | **50 credits** | `GET /companies/{company_id}/kyb-report` | A list is charged per page: walking 300 events at 100 per page is three calls. One read of `GET /events` is free: with `monitor_id`, it lists what your own monitor matched, and that is part of monitoring. ## The free plan [#the-free-plan] A personal account without a paid plan gets **50 credits once**, when the account is created (accounts that existed before 2026-10 received them when the free plan started). They expire 30 days after they are granted, and are spent before credits you bought. When a plan starts, its included credits arrive as usual; free credits you still have stay until they are spent or expire. Free credits do not pay for monitors created through the API or the MCP server; everything else they may pay for. Team workspaces do not get free credits. `GET /account` counts them in `credits.remaining`. ## When you are charged [#when-you-are-charged] Credits are deducted **before** your request runs, and refunded if it fails. The rule: * **`2xx`: charged its price.** That includes an empty result. An empty list is an answer: "this company has no shareholders on file" is worth as much as a full page. * **`4xx` and `5xx`: not charged.** A not-found company, a duplicate monitor, a validation error, a retired path's `410` or a server error costs you nothing. * **Redirects**: a `301` (a merged company, a moved path) costs nothing. A `302` that hands you a paid document is the purchase, and is charged the document's price. * **`402`**: the call never ran; nothing was deducted. * **A replay** of a request sent with an [`Idempotency-Key`](/docs/getting-started/errors#retry-writes-with-an-idempotency-key) is not charged again. Two headers say what a call cost: | Header | Sent | Value | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Registercheck-Credits-Charged` | On every response to a call made with a key, whatever its status | What this call cost. A `2xx` is charged its price, including an empty page. `0` for a free call, every `4xx` (a `404` and a `402`), every `5xx`, and a `301`. A `302` that hands you a paid document is charged. | | `Registercheck-Credits-Remaining` | Whenever the call read your balance: every charged call, and a charged call that was refunded. Free calls and a refusal before billing omit it. | Your balance after this call | Because the deduction happens first, a burst of concurrent requests can take your balance below what you expected before any of them return. ## Running out [#running-out] When your balance cannot cover a call, the API returns `402` and does not run it. Like every [error](/docs/getting-started/errors), it is a problem document; `required_credits` and `available_credits` say by how much you are short: ```json { "type": "https://docs.registercheck.de/errors/insufficient_credits", "title": "Insufficient credits", "status": 402, "detail": "Insufficient credits. This request costs 25 credits.", "code": "insufficient_credits", "request_id": "9b0c2f5e-4d1a-4c3e-9a8e-6f0b1d2c3e4f", "required_credits": 25, "available_credits": 4 } ``` Top up in the app; the new balance is live within a few seconds. You can also enable auto top-up so a balance that drops below a threshold is refilled automatically. With a paid plan, **overage** can be switched on on the [Usage (API & Nutzung)](https://registercheck.de/api-management) page: calls the balance cannot cover then go on, and are invoiced after the month at 7.5 cents a credit on Starter, 2.25 cents on Business and 0.75 cents on Scale. ## Seeing what you have used [#seeing-what-you-have-used] Your balance and a breakdown by endpoint and by key are on the [Usage (API & Nutzung)](https://registercheck.de/api-management) page. The usage breakdown is currently unreliable and may render empty even when calls have been billed. Your **balance** is always correct; if the breakdown is blank, that is a display fault and not a sign that calls were free. ## Making calls cheaper [#making-calls-cheaper] * Search first, then read. Three searches cost less than one company record. * `POST /search/companies/filter` costs 25 credits but returns up to 100 companies — far cheaper than 100 individual reads. * Cache by company `id`. Ids are stable; the record behind them rarely changes. * Use [monitoring](/docs/concepts/monitoring) instead of re-reading companies to detect change. One monitor replaces an indefinite polling loop. # Errors (/docs/getting-started/errors) Every error is an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem document, sent as `application/problem+json`. Branch on `code` — it is stable. `title` and `detail` are for people. ```json { "type": "https://docs.registercheck.de/errors/validation_failed", "title": "Validation failed", "status": 400, "detail": "1 field failed validation.", "code": "validation_failed", "request_id": "9b0c2f5e-4d1a-4c3e-9a8e-6f0b1d2c3e4f", "errors": [ { "location": "path", "name": "company_id", "detail": "is a person id (per_…); this takes a company id (cmp_…)" } ] } ``` A `400 validation_failed` lists every field that failed in `errors`: where it was (`location`: `path`, `query`, `header` or `body`), its `name`, and what is wrong with it. Quote `request_id` (also sent as the `Request-Id` header) when you contact support. Any call can answer `400`, `401`, `429`, `500` and `503`, and a call with an id in its path can answer `404`. Each operation's page in the reference lists which of `402`, `403`, `409` and `410` it can answer as well. ## Codes [#codes] | Status | `code` | Meaning | What to do | | ------ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `301` | `company_merged` | The company was merged into another. | Follow `merged_into`. | | `400` | `invalid_request` | The body cannot be parsed at all: it is not JSON, or not a JSON object. | Fix the body. | | `400` | `validation_failed` | A field failed validation: a path, query, header or body field, a cursor, or an id of the wrong kind (a person id where a company id belongs). `errors` names each one. | Fix the fields `errors` names. | | `401` | `unauthorized` | No valid API key: it is wrong, revoked, disabled or expired. | Check the `Authorization: Bearer` header and the key. | | `402` | `insufficient_credits` | Not enough credits. The call did not run and was not charged. | Top up. | | `403` | `forbidden` | Your role in the workspace does not allow this action. | Ask a workspace administrator. | | `403` | `destination_requires_plan` | Without a plan, notifications can only go to your own verified sign-in address. | Use that address, or choose a plan for more addresses and webhooks. | | `403` | `monitor_limit_reached` | You already monitor as many companies as the plan allows (`limit`; the free plan's when the workspace has none). Resuming a monitor created in the dashboard (`PATCH /monitors/{monitor_id}` with `"status": "active"`) changed nothing, and it stays paused. | Stop a monitor with `DELETE /monitors/{monitor_id}`, or pause one, or choose a plan. | | `404` | `not_found` | No such resource, or not yours — the two are indistinguishable. | Check the id. | | `409` | `conflict` | It already exists. `existing_id` names the existing resource, or is `null` when it cannot be named (two requests raced). | Use or change the existing one; list to find it when `existing_id` is `null`. | | `409` | `route_endpoint` | The destination is a monitor's delivery route: it is told at the monitor's check, always `instant`. | Change the monitor's check speed, or add your own destination for this address. | | `409` | `register_identity_ambiguous` | A register identity matched several companies. `candidates` lists them. | Retry with the `subject_id` of the one you mean. | | `409` | `idempotency_request_in_progress` | A request with this `Idempotency-Key` is still running. | Retry when it has finished. | | `410` | `endpoint_retired` | A path that was renamed (see the [migration guide](/docs/migration)) or that belongs to the retired `/api` surface. `detail` names its replacement. | Call the replacement. | | `422` | `idempotency_key_reused` | This `Idempotency-Key` was used with a different request. | Use a new key. | | `429` | `rate_limited` | Too many requests for this key, or its monthly [request allowance](/docs/getting-started/rate-limits) is used up. The key is still valid. | Wait the `Retry-After` seconds it gives, not less. For a used-up allowance that can be days. Without `Retry-After` the key does not refill: use another key. | | `500` | `internal_error` | Something failed on our side. Nothing was charged. | Retry with backoff. | | `503` | `service_unavailable` | A dependency is unavailable, including the API key check (the key was **not** rejected). | Retry with backoff; do not rotate the key. | ## Retrying [#retrying] Retry `500` and `503` with exponential backoff and jitter. A `429` says when to come back in `Retry-After`: wait that long, not less — a request made before then is refused again and still counts against your account's shared allowance. A `429` without `Retry-After` comes from a key issued with a fixed number of requests and no refill: it will not succeed again. Do not retry `400`, `401`, `402`, `403`, `404` or `422` unchanged — they will not succeed on a second attempt. ### Retry writes with an `Idempotency-Key` [#retry-writes-with-an-idempotency-key] A timeout does not mean a write failed; it may have landed. Send an `Idempotency-Key` header — a random UUID per logical request — on every `POST`, and reuse it on the retry: ```bash curl -X POST https://api.registercheck.de/v1/monitors \ -H "Authorization: Bearer $RC_KEY" \ -H "Idempotency-Key: 6f1c9f3a-8e5b-4c6d-9a2e-1b3c4d5e6f70" \ -H "Content-Type: application/json" \ -d '{"subject_id": "cmp_01h455vb4pex5vsknk084sn02q"}' ``` Within 24 hours, the same key with the same request answers the **first response** with `Idempotent-Replayed: true` — nothing is created or charged a second time. The same key with a different body answers `422 idempotency_key_reused`; a retry while the first request is still running answers `409 idempotency_request_in_progress`. Only a success (`2xx`) is stored. An error changed nothing and cost nothing, so a retry with the same key runs again — after a top-up, a `402` retry goes through instead of replaying the `402`. Creating a **webhook** destination is never replayed, because its signing secret is not stored a second time: a retry after a lost response answers `409` with `existing_id`. # Metadata (/docs/getting-started/metadata) Every object you create carries `metadata`: a map of your own string keys to string values. Put your CRM id on a monitor, your team's name on a destination, a deal stage on a list item — and read it back on every response for that object. Registercheck stores it and returns it; it never reads it or acts on it. A webhook delivery carries events, not your monitors' metadata: keep your own map from monitor id to record (you get the id back when you create the monitor), or read the monitor. It works exactly like [Stripe's metadata](https://docs.stripe.com/api/metadata), with the same limits: | Limit | Value | | --------------- | ---------------------------------- | | Keys per object | 50 | | Key length | 40 characters, no square brackets | | Value | a string of at most 500 characters | Available on [monitors](/docs/api-reference/monitors/createMonitor), [destinations](/docs/api-reference/destinations/createDestination), [lists](/docs/api-reference/lists/createList) and [list items](/docs/api-reference/lists/addListItem). ## Set it when you create [#set-it-when-you-create] ```bash curl -X POST https://api.registercheck.de/v1/monitors \ -H "Authorization: Bearer $RC_KEY" \ -H "Content-Type: application/json" \ -d '{"subject_id": "cmp_1z3gn9wpvx9j59w8bd9w5rm72n", "metadata": {"crm_id": "0015g00000XyZ", "team": "kyc"}}' ``` ## Change it with `PATCH` [#change-it-with-patch] An update is **merged** into what is stored: a key with a string value is added or replaced, a key set to `null` is removed, and keys you do not name are left alone. ```bash curl -X PATCH https://api.registercheck.de/v1/monitors/$MONITOR_ID \ -H "Authorization: Bearer $RC_KEY" \ -H "Content-Type: application/json" \ -d '{"metadata": {"team": null, "region": "sued"}}' # -> "metadata": {"crm_id": "0015g00000XyZ", "region": "sued"} ``` A request that would leave more than 50 keys, or break another limit, answers `400` and changes nothing. Metadata is returned on every read of the object, to everyone who can read it. Store references to your own records, not passwords, tokens or personal data. # Pagination (/docs/getting-started/pagination) Every list and every search answers the same envelope and pages forward with the same cursor: roles, events, documents, statements, shareholders, monitors, lists, search results and filter hits alike. | Parameter | Default | Maximum | | --------- | ------- | ------------------------------------ | | `limit` | `20` | `100` | | `cursor` | — | the `next_cursor` of the page before | The [reference-data lists](/docs/api-reference) — `/legal-forms`, `/courts`, `/role-types`, `/industries`, `/event-types`, `/event-groups` — carry no default: omit `limit` and the whole list arrives in one page, `next_cursor` `null`. Their vocabularies are fixed, and a client that renders one reads it whole — `/industries`' 2,030 codes included. Name a `limit` below the list's size and the answer pages like any other list's. ```bash curl "https://api.registercheck.de/v1/companies/{company_id}/roles?limit=50" \ -H "Authorization: Bearer $REGISTERCHECK_API_KEY" ``` ```json { "object": "page", "data": [{ "object": "role", "id": "rol_01h455vb4pex5vsknk084sn02q" }], "next_cursor": "eyJ2IjoxLCJrIjoibGlzdENvbXBhbnlSb2xlcyJ9" } ``` `next_cursor` is `null` on the last page, and that is the only end signal. Pass it back as `cursor` to get the next page: ```python def every_item(session, url, **params): cursor = None while True: page = session.get(url, params={**params, "limit": 100, **({"cursor": cursor} if cursor else {})}).json() yield from page["data"] cursor = page["next_cursor"] if cursor is None: return ``` The same loop walks every list in the API. Treat the cursor as opaque: what is inside it is not part of the contract and may change. It holds the list and its filters, not the page size, so the next page may ask for another `limit`. `POST /search/companies/filter` takes `limit` and `cursor` in the body. A cursor from a different list, or from the same list with different filters, answers `400 validation_failed` naming `cursor`, not an empty page. A caller paging with the wrong cursor has a bug, and answering with nothing would hide it. Pages only go forward. To start over, leave out `cursor`. ## What some pages add [#what-some-pages-add] * **Search and filter** add `total_count`: matches across the whole result set, or `null` when it cannot be counted cheaply. Check for `null` before showing it. * **A company's shareholders** add `extract`: how current the shareholder list behind the page is, and whether one was filed at all. See the [glossary](/docs/concepts/glossary#extract). ## Ceilings [#ceilings] Asking for `limit=101` answers `400`; it is not silently clamped. Each page is a separate billable call. Walking 4,000 search results at 100 per page is 40 calls — 40 credits for search, but 40,000 if you then read every company at 10 credits each. Consider [`POST /search/companies/filter`](/docs/concepts/filtering), which answers a structured query for 25 credits a page. ## Array parameters [#array-parameters] A parameter that takes several values repeats its name, once per value: ```bash curl "https://api.registercheck.de/v1/events?type=company.name.changed&type=company.address.changed" \ -H "Authorization: Bearer $REGISTERCHECK_API_KEY" ``` A comma-joined value (`type=company.name.changed,company.address.changed`) is not read as two values: it answers `400 validation_failed`, so it can never silently match nothing. ## Unknown parameters are rejected [#unknown-parameters-are-rejected] A query parameter the endpoint does not read answers `400 validation_failed` and costs nothing. `errors[].name` names it. `GET /formations?legal_form_code=gmbh` is refused: the parameter is `legal_form`, and `legal_form_code` is the name it had before 2026-10-02. # Quickstart (/docs/getting-started/quickstart) ## 1. Get a key [#1-get-a-key] API keys are created in the app, not through the API. Open the [Usage (API & Nutzung)](https://registercheck.de/api-management) page and sign in. Choose **Create API key**, give it a name that says where it will be used (`billing-service-prod` beats `key 1`), and create it. Copy the key. It is shown once. If you lose it, revoke it and create another — there is no way to read an existing key back. A free account comes with 50 credits for 30 days: enough to try search (1) and company reads (10). ## 2. Call the API [#2-call-the-api] Every request goes to `https://api.registercheck.de/v1` and carries your key as a bearer token. curl Python TypeScript ```bash curl "https://api.registercheck.de/v1/search/companies?name=Enduro&limit=3" \ -H "Authorization: Bearer $REGISTERCHECK_API_KEY" ``` ```python import os, requests BASE = "https://api.registercheck.de/v1" session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['REGISTERCHECK_API_KEY']}" result = session.get(f"{BASE}/search/companies", params={"name": "Enduro", "limit": 3}).json() print(result["total_count"], "matches") ``` ```typescript const BASE = 'https://api.registercheck.de/v1'; const res = await fetch(`${BASE}/search/companies?name=Enduro&limit=3`, { headers: { Authorization: `Bearer ${process.env.REGISTERCHECK_API_KEY}` }, }); const { data, total_count } = await res.json(); console.log(total_count, 'matches'); ``` Requests sent with the default `urllib.request` user agent are rejected by our CDN with a `403` and the body `error code: 1010` — before they reach the API. Use `requests`, `httpx`, or set an explicit `User-Agent` header on your `urllib` requests. ## 3. Read the response [#3-read-the-response] `/search/companies` returns candidates with a stable `id`. That `id` is what every other endpoint takes. ```json { "object": "page", "data": [ { "object": "summary", "type": "company", "id": "cmp_63b6n0c6p09pnsrr6nrtd0dzsc", "name": "Enduro Reise & Training GmbH", "status": "active", "legal_form": "gmbh", "register": { "court": { "code": "D2505", "name": "Amtsgericht Memmingen" }, "type": "HRB", "number": "13557", "formatted": "Amtsgericht Memmingen HRB 13557", "country": "DE" }, "address": { "city": "Memmingen" }, "registration_date": null, "founding_date": "2011-06-14" } ], "next_cursor": null, "total_count": 4 } ``` Every list answers in this shape: the items in `data`, and `next_cursor` to walk further (`null` on the last page). A register number is a **string** here and everywhere else — `"13557"`, not `13557` — because a register number is an identifier, not a quantity. ## 4. Fetch the full record [#4-fetch-the-full-record] ```bash curl "https://api.registercheck.de/v1/companies/cmp_63b6n0c6p09pnsrr6nrtd0dzsc" \ -H "Authorization: Bearer $REGISTERCHECK_API_KEY" ``` You get the register entry as it stands today: name, registered office and address, stated purpose, share capital, legal form, register identity, and a `counts` block saying how many current roles, documents and financial statements exist — so you know whether a sub-resource is worth a call before you spend a credit on it. Roles, documents, statements, ownership and history are separate endpoints rather than one large object. Add `?expand=roles` if you want the first page of roles inline at no extra charge. ## Next [#next] # Rate limits (/docs/getting-started/rate-limits) Three limits sit in front of the API. They count requests, free ones included; credits are separate and only paid calls spend them ([Credits and billing](/docs/getting-started/credits-and-billing)). | Limit | Window | Applies to | | ------------------------------------- | ------------------------------------------------ | ----------------------- | | 60 requests per minute | rolling 60 seconds | each key | | The monthly request allowance | calendar month, refilled on the 1st at 00:00 UTC | each key | | The same allowance across the account | rolling 30 days | all keys on the account | The request allowance follows your workspace's plan: **150,000 requests a month with a plan, 9,000 without one**. Credits do not change it: they are spent call by call, and a call without credits answers `402`. A key is issued with its workspace's allowance. Keys issued before the allowance followed the plan are raised to it, and the account and its keys are raised again when a plan starts. Nothing is ever lowered. The account-wide allowance counts the last 30 days, not the calendar month. After a busy end of a month it can still be used up when a key refills on the 1st; it frees up as the requests of 30 days ago fall out of the window, and its `429` says when in `Retry-After`. Exhausting any of them answers `429` with code `rate_limited` and a `Retry-After` header; the key is still valid. For a key's own allowance, `Retry-After` and `X-RateLimit-Reset` give that key's next refill (a key issued with a fixed number of requests and no refill says so, without `Retry-After`). Another key has its own monthly allowance but shares the account's, so it does not help once the account's is spent. Raising an allowance beyond your plan's: [sales@registercheck.de](mailto:sales@registercheck.de). ## Response headers [#response-headers] Successful responses carry the per-minute limit state: ```http X-RateLimit-Limit: 60 X-RateLimit-Remaining: 58 X-RateLimit-Reset: 2026-09-15T08:41:00Z X-RateLimit-Provider: Unkey ``` These headers describe the limit closest to being hit. The monthly allowance's balance is not currently exposed on a successful response. ## Staying inside the limits [#staying-inside-the-limits] * Serialise your calls or cap concurrency at around 40 requests per minute; the per-minute ceiling is shared, so two workers at 40 each will trip it. * Cache company records. Register entries change on the order of once a year, not once a minute — see [Monitoring](/docs/concepts/monitoring) for the supported way to learn about changes instead of polling. * Prefer one [filter](/docs/api-reference/search/filterCompanies) call over many single-company reads when you are building a list. # Companies (/docs/concepts/companies) ## Where a company record comes from [#where-a-company-record-comes-from] Two sources, and the difference matters: 1. **The register index** — name, court, register number, legal form, status. Present for every company, refreshed as the register changes. 2. **The register extract** — address, purpose, capital, representatives, founding date. Only present once the extract has been fetched and successfully parsed for that company. A company with only source 1 is not an error. It is a company whose extract has not been retrieved. See [Data coverage](/docs/concepts/data-coverage) for how often that is the case. ## The record [#the-record] ```json { "id": "cmp_63b6n0c6p09pnsrr6nrtd0dzsc", "object": "company", "name": "Enduro Reise & Training GmbH", "status": "active", "legal_form": "gmbh", "register": { "court": { "code": "D2505", "name": "Amtsgericht Memmingen" }, "type": "HRB", "number": "13557", "formatted": "Amtsgericht Memmingen HRB 13557", "country": "DE" }, "address": { "street": "In der Paint 14", "postal_code": "87730", "city": "Bad Grönenbach", "state": "DE-BY", "country": "DE", "formatted": "In der Paint 14, 87730 Bad Grönenbach", "coordinates": { "latitude": 47.87, "longitude": 10.25 } }, "purpose": "Planung und Durchführung von Motorrad-Sicherheitstraining …", "capital": { "amount": "25000.00", "currency": "EUR" }, "representation_rule": "Ist ein Geschäftsführer bestellt, so vertritt er allein.", "identifiers": { "euid": "DEM1401.HRB13557", "lei": null }, "industries": [{ "system": "wz2025", "code": "85.53", "name": "Fahr- und Flugschulen", "level": 4 }], "registration_date": null, "founding_date": "2008-11-21", "termination_date": null, "last_register_entry_date": "2025-01-26", "counts": { "current_roles": 1, "documents": 4, "financial_statements": 15 } } ``` The record is the register entry **as it stands today**. Roles, documents, statements, ownership and history are separate endpoints, which is why `counts` is here: it tells you whether calling one is worth a credit before you spend it. ### `register` [#register] The identity of the entry. `formatted` is the one-line form you would quote to a lawyer. `court.name` is given as the register writes it (`Amtsgericht Berlin (Charlottenburg)`), which is not always the court's colloquial name; `court.code` is the official court code, stable across spellings, and is what you should match on. `number` is a **string**, here and everywhere else. A register number is an identifier, not a quantity — it can carry letter suffixes, and leading zeros are meaningful. [`GET /courts`](/docs/api-reference/reference-data/listCourts) lists the court codes this API names, with the name the register uses. It is not every court the portal's form mapping includes. ### `name` [#name] The full legal name including the legal form. Former names are not in this object — they are in the [register history](/docs/api-reference/companies/listCompanyEvents), as `company.name.changed` events carrying what the entry replaced. ### `status` [#status] One of `active`, `liquidation`, `terminated` or `unknown`, always lowercase. One rule derives it for the company record, search results, suggestions and filter hits, so a company has the same `status` in all of them. `unknown` means what it says: the register data does not tell us. It is not a synonym for inactive, and the field will never claim `active` for a company we cannot confirm is trading — for a KYC decision, a wrong `active` is the one answer you cannot absorb. The retired `/api` derived this from a comparison that never matched, so nearly every company was reported `active`, including entries the register had deleted. v1 reads the stored value, and falls back to `unknown` rather than guessing. ### `legal_form` [#legal_form] A plain value to branch on — `gmbh`, `gmbh_co_kg`, `ug`, `ev` and so on. The German name, the usual abbreviation (`GmbH`) and the ISO 20275 ELF code, where one applies, are in [`GET /legal-forms`](/docs/api-reference/reference-data/listLegalForms): free, the same for every caller, so read it once and look the value up. The register stores this as free text with more than 20,000 distinct spellings, so `legal_form` is `null` for the 7.6% of companies whose wording we cannot classify confidently. A null is "we could not read it", never "it has no legal form". ### `industries` [#industries] WZ 2025 codes assigned from the registered business purpose. They are not register data. Each entry on a company is `{system, code, name, level}`. The classification itself — every code, its German title, its level and its parent — is [`GET /industries`](/docs/api-reference/reference-data/listIndustries): free, the same for every caller. ### `address` [#address] The registered office as registered, which is the address for service — not necessarily a trading address. `formatted` is the single-line form, and `state`, when present, is the federal state as an ISO 3166-2 code (`DE-BY`). Any component may be `null`; `city` is by far the most commonly populated. ### `capital` [#capital] Registered share capital (*Stammkapital* / *Grundkapital*), in the currency stated in the register. The amount is a **decimal string** so it never loses precision in a JSON parser that reads numbers as floats. Pre-2002 entries can still be denominated in `DEM`. This is the capital the register records, which is not the same as equity, and is never updated by trading. ### `registration_date` and `founding_date` [#registration_date-and-founding_date] Two dates, and they are not the same: * `registration_date` is the day of the company's first register entry. It is `null` while it cannot be read for a company; [`GET /formations`](/docs/api-reference/formations/listFormations) carries it for every new registration. * `founding_date` is the date of the founding contract (the articles of association), as the register extract states it. It usually precedes `registration_date` by days or weeks. Neither is wrong. Use `registration_date` when you mean "since when does the register know this company", and `founding_date` when you mean "when was it founded". ### `counts` [#counts] How much related data exists: current roles, documents on file, financial statements. Counted from the record itself, so it costs nothing extra. A zero means there is nothing to fetch, which is worth checking before spending a credit on the sub-resource. ### Roles are their own endpoint [#roles-are-their-own-endpoint] Managing directors, board members and authorised signatories are one list at [`GET /companies/{company_id}/roles`](/docs/api-reference/companies/listCompanyRoles), because to a caller they are the same thing: a person or a company holding a role. Each `role` names its `holder` as a party, so a role held by a company reads like one held by a person. Narrow the list with `role_type`, or add `expand=roles` to the company request for the first page inline. An empty list means "no extract parsed", not "nobody is in charge" — check whether `address` and `purpose` are also null before concluding anything. # Data coverage (/docs/concepts/data-coverage) Coverage is uneven, and the uneven parts are predictable. The figures below are counted directly against the production database, not estimated. They were last measured on **15 September 2026** against **5,796,418 company records**. ## Start here: active versus terminated [#start-here-active-versus-terminated] Half the register is historic. 2,932,047 records (50.6%) are companies whose register entry has been deleted — many of them decades old, entered when the register was digitised, and never accompanied by a filed extract. If you quote a single coverage number you will quote the wrong one. **Filter to active companies before judging coverage.** | Field | All 5.80M records | Active companies only (2.86M) | | -------------- | ----------------- | ----------------------------- | | Address | 41.6% | **75.9%** | | Founding date | 39.8% | **74.7%** | | Stated purpose | 36.3% | **71.3%** | | Share capital | 30.5% | **56.8%** | Name, legal form, register court and register number are populated for essentially every record — those come from the register index and do not depend on a document being fetched. ## Ownership and financials [#ownership-and-financials] These depend on a filing existing at all, so the ceiling is set by German law rather than by us. | Data | Active companies with it | | ----------------- | ------------------------ | | Shareholder data | 22.7% (649,529) | | Financial reports | 29.8% (853,333) | A `GmbH` files a shareholder list; an `AG` does not. A chain of holders also ends at a company registered abroad, and at a holder we have not been able to link to a record. A company below the size thresholds files an abridged balance sheet; a sole trader files nothing. Most of the remaining 77% is not a gap we can close — it is data that was never required to exist. ## Why a field is empty [#why-a-field-is-empty] Three different causes, and they are worth telling apart: **No document has been fetched.** Around 3 million records, overwhelmingly terminated entries, have no register extract on file. There is nothing to extract from, and for a deleted entry the register usually has nothing to give either. Asking us to fetch one is what [jobs](/docs/roadmap#jobs-fetching-data-again) will be for; they are not available yet. **The document is on file but the field was not extracted.** Of the 2,795,875 companies that hold a register extract, 21.3% still have no address, 22.5% no founding date and 41.1% no capital. This is our extraction gap, and it is the one we are working on. **The field does not apply.** No shareholder list for an `AG`, no capital for an `e.K.`, no accounts for a company with no filing obligation. If `address`, `purpose` and `capital` are all `null` on a company record, assume case 1 or 2 — no extract has been parsed. If some are populated and others are not, the missing ones are usually case 3. ## Freshness [#freshness] Register entries are re-read on a cycle rather than pushed to us by the courts. `last_register_entry_date` on the company record tells you when the register itself last recorded a change, which is the number to show your users. Shareholdings are as fresh as the last filed list, which may be many years old — always read each shareholding's `source.date` alongside the figures. Financial reports appear 11–23 months after the fiscal year they cover, which is when the filing obligation falls due. ## What we do not have [#what-we-do-not-have] * **The Transparenzregister.** It is not public data and is not in this API. [`GET /companies/{company_id}/beneficial-owners`](/docs/api-reference/companies/getComputedBeneficialOwners) computes an indicative result from shareholder lists and register entries. It may be incomplete, and it is not that register. * **Shareholders of an `AG`.** Not filed. The computed analysis says `partial`, with `stock_corporation`, when the company is an AG, SE or KGaA. * **Trading addresses.** The register holds the address for service, which is often a lawyer's or accountant's office. * **Anything not filed with a German court.** No credit scores, no trading history, no employee counts from the register itself. # Filtering (/docs/concepts/filtering) [`POST /search/companies/filter`](/docs/api-reference/search/filterCompanies) answers a structured question — "GmbHs in Bavaria, founded since 2025, with a managing director over 60" — rather than a text query. It costs 25 credits a page. ## Shape [#shape] ```json { "company": { "…": "criteria on the register entry itself" }, "financials": { "…": "criteria on the most recent statement" }, "roles": { "conditions": [] }, "shareholders": { "conditions": [] }, "limit": 20 } ``` Four groups, and at least one is required — an empty filter is a `400`, not the whole register. Everything you give must hold: the groups combine with **AND**, and so do the fields inside `company` and `financials`. ## Criteria on the company [#criteria-on-the-company] ```json { "company": { "status": ["active"], "legal_form": ["gmbh", "gmbh_co_kg"], "city": { "equals": "Berlin" }, "founding_date": { "gte": "2025-01-01" }, "capital": { "gte": 25000, "lte": 100000 } }, "limit": 20 } ``` Three kinds of value, and which one a field takes is fixed: | Kind | Looks like | Used by | | ----- | ----------------------------------------------------------------------- | --------------------------------------------------------------------- | | A set | `["gmbh", "ug"]` — any of these | `status`, `legal_form`, `register_court`, `register_type`, `industry` | | Text | `{"equals": …}`, `{"contains": …}` or `{"starts_with": …}`, exactly one | `name`, `city`, `state`, `purpose` | | Range | `{"gte": …}`, `{"lte": …}`, or both | `founding_date`, `capital` | `near` is its own thing: `{"latitude": 48.13, "longitude": 11.58, "radius_meters": 5000}`. ## Criteria on roles and shareholders [#criteria-on-roles-and-shareholders] These take **conditions**, because a company has many of each and it matters whether one role holder satisfies the whole condition or three holders satisfy a third of it each. A role is an office in the company: who acts for it. ```json { "roles": { "conditions": [ { "last_name": { "contains": "Müller" }, "status": "current" } ] }, "limit": 20 } ``` Every field inside one condition must match **the same role**. The example finds companies where someone named Müller holds an office now, not companies with a Müller and, separately, someone current. * `name` matches the name of a company that holds a role, or a person's first, last or full name (`"Hans Müller"`); `first_name` and `last_name` match one of a person's names alone. All three match a substring (`contains`). * `role_type` can only be `["authorized_signatory"]` (Prokuristen) for now: the register data records no role type for the other roles, so any other value is answered `400`. * `age` is whole years as of today in Europe/Berlin, from 0 to 120. A company matches when the role holder's age is inside the bounds (`gte` and `lte`, one or both, inclusive). An age on its own answers `400`: combine it with a narrowing criterion, for example a name, a register, a location or another company filter. A role's, a shareholder's or the company's `status` does not count: it leaves almost every row in the scan. A shareholder's `holder_type` does not count either: it only picks person or company holders. A shareholder criterion, such as a `person_id`, does count for a role's age. The same rule covers a Kommanditist's `age` or `birth_date` (`type: ["limited_partner"]` only picks that table). A fraction, a value outside 0–120, or `gte` greater than `lte` also answers `400`. Plain shareholders have no date of birth, so `age` there answers `400`. ```json { "company": { "city": { "equals": "München" } }, "roles": { "conditions": [{ "age": { "gte": 60, "lte": 69 } }] } } ``` * For the companies of one known person, a condition takes a `person_id`: see [Find a person's companies](/docs/guides/find-a-company#find-a-persons-companies). `match` says how several conditions combine (`all`, the default, or `any`), and `min_matching` / `max_matching` count how many role holders must meet a condition. The data store cannot combine conditions yet, so a criterion with more than one condition, or with a matching bound, is answered `400`: send one condition. `shareholders` works the same way, with `type`, `holder_type` (`person` or `company`), `percentage`, `held_since`, `tenure_years` and `status` (`current` or `former`). Filtering `type: ["limited_partner"]` selects Kommanditisten. ## When a filter cannot be expressed [#when-a-filter-cannot-be-expressed] Some questions have no honest translation, and those return `400` with a problem document saying so rather than a result that quietly means something else: * **`status: ["unknown"]`.** `active`, `liquidation` and `terminated` are selected on the status each company's record publishes, alone or together, so an `active` segment never contains a company in liquidation and `total_count` is the size of the segment. `unknown` is what a company says when the register data gives no status at all, and it cannot be asked for as a segment. * **`legal_form: ["foreign_entity"]`.** Foreign forms are written 1,281 different ways in the register, so there is no set of spellings to match on. A filter that silently widened would be worse than one that refuses: you would build a segment on it and never know. ## Paging [#paging] `limit` and `cursor` in the body page the result set: send each page's `next_cursor` back as `cursor` — see [Pagination](/docs/getting-started/pagination). Each call costs 25 credits regardless of how many results come back, so prefer a larger `limit` over many small pages. # Financial statements (/docs/concepts/financials) ## What exists [#what-exists] German companies above certain thresholds must publish annual accounts (*Jahresabschluss*) in the *Bundesanzeiger*. Small companies file an abridged balance sheet with no profit and loss account. Sole traders and most partnerships file nothing. So the honest description of this endpoint is: **the accounts that were filed, for the companies that had to file them.** ## Reading financial statements [#reading-financial-statements] [`GET /companies/{company_id}/financial-statements`](/docs/api-reference/companies/listCompanyFinancialStatements) returns the statements and the figures extracted from them. ```json { "object": "page", "data": [ { "id": "4d5e6f7a-8b9c-4d0e-8f1a-2b3c4d5e6f7a", "object": "financial_statement", "company_id": "3f1c2a9e-5b7d-4c8a-9e21-6d4f0b8a1c55", "title": "Jahresabschluss zum Geschäftsjahr vom 01.01.2024 bis zum 31.12.2024", "period": { "fiscal_year": 2024, "start_date": "2024-01-01", "end_date": "2024-12-31" }, "published_date": "2025-11-20", "consolidated": false, "language": "de", "links": { "download": null, "source": "https://www.unternehmensregister.de/" }, "figures": { "currency": "EUR", "employees": null, "other_financial_obligations": null, "balance_sheet": { "total_assets": "184320.55", "fixed_assets": "12400.00", "current_assets": "171920.55", "cash": "96512.10", "total_equity_and_liabilities": "184320.55", "equity": "61880.30", "liabilities": "110440.25", "provisions": "12000.00", "subscribed_capital": "25000.00", "capital_reserve": null, "bank_loans": null, "liabilities_to_shareholders": null }, "income_statement": null } } ] } ``` `links.source` is the publication in the Unternehmensregister. `links.download` is where to download our copy, and stays `null` until downloading by statement id is available. Check `counts.financial_statements` on the [company record](/docs/api-reference/companies/getCompany) before calling this endpoint: a zero means nothing has been filed, and the call would cost 10 credits to tell you so. ## What the figures mean [#what-the-figures-mean] * Figures are taken from the filed document. Where a company filed an abridged balance sheet, only balance-sheet lines exist; revenue and profit are genuinely absent, not missing. * `period.fiscal_year` is the year the accounts cover. `published_date` is when they appeared, typically 11–23 months later. * Accounts are filed in the currency of the filing. These numbers are read out of filed documents by an automated pipeline. They are good enough to screen and to trend. They are not a substitute for the filed document itself for any decision that has to survive challenge — and the document is what your counterparty will produce. ## Absence is not zero [#absence-is-not-zero] A company with no reports is usually a company with no filing obligation. Do not read a missing balance sheet as a balance sheet of zero, and do not rank companies by a figure that most of them are not required to publish. # Glossary (/docs/concepts/glossary) The API uses one word for each thing the register holds, and it is the same word everywhere: in the path, in the `object` field of the response, in the id prefix, in the [API reference](/docs/api-reference) and on these pages. If you find two words for one thing, or one word for two things, that is a bug — [tell us](/docs/support). ## The words [#the-words] | German | Word and `object` | Id | Paths | | ----------------------------------------------------------------------------------------------- | --------------------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | Unternehmen | `company` | `cmp_…` | `/companies/{company_id}` | | Person (natürliche Person) | `person` | `per_…` | `/persons/{person_id}` | | Funktion: Geschäftsführer, Vorstand, Prokurist, Liquidator, persönlich haftender Gesellschafter | `role` | `rol_…` | `/companies/{company_id}/roles`, `/persons/{person_id}/roles` | | Beteiligung, Kommanditeinlage | `shareholding` | `shr_…` | `/companies/{company_id}/shareholders`, `/companies/{company_id}/holdings`, `/persons/{person_id}/holdings` | | Beteiligungsbaum | `ownership_tree` | — | `/companies/{company_id}/ownership-tree` | | Wirtschaftlich Berechtigte, rechnerisch | `beneficial_owner_analysis` | — | `/companies/{company_id}/beneficial-owners` | | KYB-Bericht | `kyb_report` | `kyb_…` | `/companies/{company_id}/kyb-report` | | Änderung, Ereignis | `event` | `evt_…` | `/events`, `/events/{event_id}`, `/companies/{company_id}/events` | | Registereintragung | `register_entry` | `rge_…` | inside an event's `source` | | Ereignistyp | `event_type` | its name, e.g. `company.name.changed` | `/event-types` | | Ereignisgruppe | `event_group` | its name, e.g. `representation` | `/event-groups` | | Überwachung | `monitor` | `mon_…` | `/monitors` | | Zustellziel (E-Mail-Adresse, Webhook-URL oder Slack-Kanal) | `destination` | `dst_…` | `/destinations` | | Zustellung | `delivery` | `dlv_…` | `/deliveries`; also the body of every webhook | | Liste | `list` | `lst_…` | `/lists` | | Listeneintrag | `list_item` | `lsi_…` | `/lists/{list_id}/items` | | Dokument (Registerauszug, Gesellschafterliste, Satzung …) | `document` | `doc_…` | `/companies/{company_id}/documents` | | Jahresabschluss | `financial_statement` | `fst_…` | `/companies/{company_id}/financial-statements` | | Neugründung | `formation` | `cmp_…`, the company's own id | `/formations` | | Kurzprofil (Suchtreffer, Vorschlag) | `summary` | the company's or person's id | `/search/companies`, `/search/persons`, `/search/suggestions`, `/search/companies/filter` | | Konto | `account` | `wsp_…`, your workspace | `/account` | | Rechtsform | `legal_form` | its code, e.g. `gmbh` | `/legal-forms` | | Registergericht | `court` | its code, e.g. `D2601` | `/courts` | | Funktionsart (Geschäftsführer, Vorstand, Prokurist, …) | `role_type` | its code, e.g. `managing_director` | `/role-types` | | Wirtschaftszweig (WZ 2025) | `industry` | its code, e.g. `62.10` | `/industries` | A **role** is an office: someone who acts for the company. A **shareholding** is an interest: someone who holds part of it. A Kommanditist holds a `shareholding` of `type: limited_partner` and no role. A Komplementär appears twice, because the register states both: a `general_partner` role (they represent the company) and a `general_partner` shareholding (they hold an interest in it). The direction of a shareholding is in the path, not in the object: `/companies/{company_id}/shareholders` lists who holds shares **in** that company; `/companies/{company_id}/holdings` and `/persons/{person_id}/holdings` list what that company or person holds **in others**. Every one of them returns `shareholding` objects. The chain behind a monitor is **monitor → event → delivery → destination**: a monitor matches events about its company, and each delivery sends one or more of those events to one destination. ## Ids [#ids] Every id is the kind of thing it names, then `_`, then 26 characters, for example `cmp_01h2xcejqtf2nbrexx3vqjhp41`. The prefix is part of the id: store the whole string. An endpoint that takes a company id refuses a person id with `400 validation_failed`, so a mixed-up id fails loudly instead of answering `404`. Codes keep their own form: legal forms (`gmbh`), courts (`D2601`), role types (`managing_director`), event types (`company.name.changed`), event groups (`representation`), register types (`HRB`). Ids are [TypeIDs](https://github.com/jetify-com/typeid/blob/main/spec/README.md) (version 0.3.0). If you stored ids before they had prefixes, the [migration guide](/docs/migration#ids) converts them. A WZ 2025 code keeps the form Destatis publishes (`A`, `62.10`, `01.11.0`). [`GET /industries`](/docs/api-reference/reference-data/listIndustries) lists every code, with its German title, its level and its parent. ## Shapes that recur [#shapes-that-recur] ### `party` [#party] Wherever a response names a company or a person it is not itself about — the holder of a role, the holder of a shareholding, the subject of an event — it uses the same three fields: ```json { "object": "person", "id": "per_01hxn3rys1fh58w4d1pb1x9s82", "name": "Erika Mustermann" } ``` `object` is `company` or `person`. `id` is `null` when the register names a holder we could not match to a record, such as a company registered abroad; `name` is then the name as filed. For anything else about the party, read its own record. ### `subject` [#subject] The company or person an event, a monitor or a list item is about. It is a `party`. When you create one of those, you send `subject_id`. ### `page` [#page] Every list and every search answers the same envelope: ```json { "object": "page", "data": [], "next_cursor": "eyJ2IjoxLCJrIjoi…" } ``` Search adds `total_count`. A page of shareholders adds `extract`. See [Pagination](/docs/getting-started/pagination). ### `extract` [#extract] How current the data behind a page is, because shareholders are read from filed documents: | `status` | Meaning | | ------------- | ---------------------------------------------------------------------------------------- | | `parsed` | The latest filed document has been read; `fetched_at` says when and `document_id` which. | | `processing` | We are reading it now. Ask again in a few minutes. | | `not_filed` | The register holds no such document for this company. An empty page is the answer. | | `not_fetched` | We have not fetched it from the register. | | `failed` | We fetched it and could not read it. | ### `summary` [#summary] What search returns: a short profile with `object: "summary"` and `type: "company"` or `type: "person"`. Every field in it has the same name and meaning as in the full record, so a summary is a strict subset of a `company` or `person`. Read the full record by its `id`. ### Errors [#errors] An error is a problem document ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)) with a `code`, not an object with an `object` field. See [Errors](/docs/getting-started/errors). ## Register identity [#register-identity] A company is entered at one register court (`register.court`), in one register type (`register.type`), under one number (`register.number`); `register.formatted` writes the three the way it is usually cited, e.g. `Amtsgericht München HRB 999999`. A few old entries carry no register type; their `register.type` is `null`. | `register.type` | Register | | --------------- | -------------------------------------------------------------------------- | | `HRA` | Handelsregister Abteilung A: sole traders and partnerships (e.K., OHG, KG) | | `HRB` | Handelsregister Abteilung B: corporations (GmbH, UG, AG, KGaA, SE) | | `GnR` | Genossenschaftsregister: cooperatives | | `PR` | Partnerschaftsregister: partnerships of the liberal professions | | `VR` | Vereinsregister: registered associations | | `GsR` | Gesellschaftsregister: registered civil-law partnerships (eGbR) | A register number is unique only within one court and one type, so the company `id` is the key to store. ## Words you will not find [#words-you-will-not-find] Some words the API used before October 2026 are gone, each replaced by one of the words above. The [migration guide](/docs/migration) lists every one, old next to new. # The data model (/docs/concepts) The German commercial register is not a database. It is \~170 courts keeping their own books, in prose, with their own conventions. Everything this API returns is an interpretation of those books, and the words below mean something specific. Reading this page once will save you from most of the surprises. Each word is used for one thing only, in the paths, the `object` field, the ids and the reference; the [glossary](/docs/concepts/glossary) lists them side by side. ## What the register holds [#what-the-register-holds] ### Company [#company] A **company** (`cmp_…`) is one entry in one court's register — identified by court, register type (`HRA`, `HRB`, `GnR`, `PR`, `VR`, `GsR`) and register number. That is narrower than "a business". A group with branches registered at three courts is three companies here, each with its own id, because that is how the register holds it. `Siemens Aktiengesellschaft Zweigniederlassung Rostock` is a separate register entry from `Siemens Aktiengesellschaft`. Register numbers are not unique on their own — HRB 670 exists at most courts — so the `id` is the only safe key to store. See [Companies](/docs/concepts/companies). ### Person [#person] A **person** (`per_…`) is a natural person named in a register entry: a managing director, a board member, a holder of *Prokura*, a shareholder, a partner. Register entries name people by name, and frequently by name and city and date of birth. People are matched across companies where the register gives enough to match on. Where it does not, the same human may appear as two persons. Treat a person id as "this named holder", not as a verified identity. See [Persons](/docs/concepts/people). ### Role [#role] A **role** (`rol_…`) is an office a person or a company holds at a company: Geschäftsführer, Vorstand, Prokurist, Liquidator, persönlich haftender Gesellschafter. Roles start and end, and the register records both, so a role that has ended is still part of the record, with `status: former`. The same role object is listed from both sides: `/companies/{company_id}/roles` and `/persons/{person_id}/roles`. ### Shareholding [#shareholding] A **shareholding** (`shr_…`) is an interest one party holds in a company, taken from the shareholder list (*Gesellschafterliste*) a GmbH must file, or from the register entry of a partnership. The `holder` may be a person or another company — which is what makes ownership chains possible. A Kommanditist's interest is a shareholding with `type: limited_partner`. Shareholder lists are filed as documents, not as data. What you get back is what was extracted from the most recent filed list, which may be years old, and which may not exist at all; the page's `extract` says which. The **ownership tree** (`/companies/{company_id}/ownership-tree`) follows linked companies for up to three levels and computes each holder's effective percentage along that path. [`GET /companies/{company_id}/beneficial-owners`](/docs/api-reference/companies/getComputedBeneficialOwners) is the separate computation of beneficial owners from those filings. See [Ownership](/docs/concepts/ownership). ### Document [#document] A **document** (`doc_…`) is a filing: the register extract (*Registerauszug*), the list of shareholders, the articles of association. Documents are fetched from the register and extracted into data. ### Financial statement [#financial-statement] A **financial statement** (`fst_…`) is a filed set of annual accounts (*Jahresabschluss*) and the figures extracted from it. See [Financial statements](/docs/concepts/financials). ### Event [#event] An **event** (`evt_…`) is one change to a company: a new name, a new address, a change of capital, a managing director appointed or removed. Its `type` says which change (`company.name.changed`); its `subject` is the company; its `data` holds what changed, shaped by the type; its `source` is the register entry (*Eintragung*, `rge_…`) it was read from. One register entry often states several changes — a new managing director, a new address and new articles in one *Eintragung* — so one register entry can be the source of several events. Read them across all companies at `/events`, or for one company at `/companies/{company_id}/events`. ### Formation [#formation] A **formation** is a company that was just entered for the first time (*Neugründung*). It is addressed by its company's `cmp_…` id. See [Follow new formations](/docs/guides/follow-new-formations). ## What you create [#what-you-create] These belong to your workspace, and reading or changing them is free. * A **monitor** (`mon_…`) watches one company for the event types you choose. * A **destination** (`dst_…`) is where matched events are sent: an e-mail address, a webhook URL or a Slack channel, with a cadence. * A **delivery** (`dlv_…`) is one send of one or more events to one destination. A webhook body is a delivery. The chain is **monitor → event → delivery → destination**; see [Monitoring](/docs/concepts/monitoring) and [Webhooks](/docs/concepts/webhooks). * A **list** (`lst_…`) is your own set of companies and persons, each one a **list item** (`lsi_…`). ## How they connect [#how-they-connect] Every reference from one object to a company or person is a **party**: `{object, id, name}`. A role's `holder`, a shareholding's `holder` and an event's `subject` all have that shape, so one piece of code reads them all. Follow the `id` to the full record. Every list answers a **page**, `{object: "page", data, next_cursor}`, and search answers a page of **summaries**: short profiles whose fields are a strict subset of the full record. See [Pagination](/docs/getting-started/pagination). ## A word that means less than you think [#a-word-that-means-less-than-you-think] `status` describes the **register entry**, not the business. It is one of four lowercase values: `active`, `liquidation` (the entry is live, the company is being wound up), `terminated` (the entry has been deleted, *gelöscht*) or `unknown` (the register data does not say). The company record, search results, suggestions and the structured filter derive it with the same rule, so a company has the same `status` wherever you read it; see [Companies](/docs/concepts/companies). # Monitoring (/docs/concepts/monitoring) A **monitor** ties your account to one company. When its register entry changes, the monitor matches an **event**, listed free by [`GET /events?monitor_id=`](/docs/api-reference/events/listEvents), and sends it to your **destinations** as a **delivery**, which [`GET /deliveries`](/docs/api-reference/deliveries/listDeliveries) lists. The chain is monitor → event → delivery → destination. This is the supported alternative to polling. Re-reading a company record on a schedule costs 10 credits a time and will usually tell you nothing changed. ## Creating a monitor [#creating-a-monitor] Name the company the way you already know it — court, register and number — and skip the search: ```bash curl -X POST "https://api.registercheck.de/v1/monitors" \ -H "Authorization: Bearer $REGISTERCHECK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "register": {"court": "München", "type": "HRB", "number": "999999"}, "event_types": ["representation", "capital"], "metadata": {"crm_id": "0015g00000XyZ"} }' ``` Or by its id, if you have one: `{"subject_id": "cmp_01hxn3rys1fh58w4d1pb1x9s81"}`. Give one, not both. `register` has the shape of a company's own `register`; the monitor names its company in `subject`. * `register.court` takes the court's code (`D2601`), its name (`Amtsgericht München`) or its town (`München`). A name that fits two courts ("Frankfurt") is refused with both named. * An identity that matches no company answers `404`. About 1% of register identities are shared by more than one entry; those answer `409 register_identity_ambiguous` with every candidate, and you retry with the `subject_id` of the one you mean. * Leave `event_types` out and you get `["*"]` — every type, including ones added later. * [`metadata`](/docs/getting-started/metadata) is yours: a CRM id, a team, anything to find the monitor again. The monitor always belongs to the account that owns the API key. Creating a second monitor on the same company answers `409` with the existing one's id in `existing_id`. ## Event types [#event-types] Name event types, or a category that stands for several — the same groups the app shows: | Category | Event types | Works today | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | | `representation` | `company.role.added`, `company.role.removed` — managing directors, board members and *Prokuristen* — and `company.representation_rule.changed` | yes | | `capital` | `company.capital.changed` | yes | | `registered_office` | `company.registered_office.changed` (registered office moved), `company.address.changed`, `company.register_court.changed` | yes | | `identity` | `company.name.changed`, `company.legal_form.changed`, `company.purpose.changed`, `company.articles.changed` | yes | | `conversion` | `company.merger.recorded`, `company.transformation.recorded` | yes | | `insolvency` | `company.insolvency.recorded` — opened, rejected for lack of assets, ended, provisional measures | yes | | `dissolution` | `company.liquidation.recorded` (dissolved), `company.deregistered` (deleted) | yes | | `other_entries` | `company.legal_relationship.recorded`, `company.note.recorded` | yes | | `shareholders` | `company.shareholder.added`, `company.shareholding.changed`, `company.shareholder.removed` | not yet | | `financial_statement` | `company.financial_statement.published` | not yet | | `*` | every event type, including ones added later | — | A register entry has the same type here as in the [register history](/docs/api-reference/companies/listCompanyEvents): both read what the entry says, not only the column it was filed under. A deletion is `company.deregistered` — the register files most of them as a "liquidation" — and an articles change is `company.articles.changed`, not a change of legal form. A category becomes the event types it covers, and the monitor shows those. Types that do not work yet are accepted — the monitor starts delivering them when their source is switched on — and every monitor lists them in `unavailable_event_types`, so you are never left waiting for an event that cannot come. [`GET /event-types`](/docs/api-reference/reference-data/listEventTypes) lists every type with its labels: `monitorable` says whether a monitor can ask for it, and `available` and `unavailable_reason` say whether it is delivered yet. [`GET /event-groups`](/docs/api-reference/reference-data/listEventGroups) lists the categories. Beneficial ownership (`beneficial_owners`) is not offered: nothing detects it, the group is listed with `available: false`, and asking for it answers `400` with the reason. A monitor reports what the register enters after it was created. An entry dated more than 14 days before you created the monitor is not sent — it is history you could read when you started monitoring — even when we only find it now, as happens when a company's register sheet is read again. An authorised signatory holds a role here, as everywhere else in the API: `company.role.added` covers both a board appointment and a grant of *Prokura*, and the event's `data.role_type` says which. Creating a monitor costs 10 credits; every other monitor operation is free. Send an [`Idempotency-Key`](/docs/getting-started/errors#retry-writes-with-an-idempotency-key) so a retry after a timeout cannot create or charge twice. ## Managing monitors [#managing-monitors] | Operation | Endpoint | Cost | | ---------------------------------- | --------------------------------------------------------------------- | ---- | | List | [`GET /monitors`](/docs/api-reference/monitors/listMonitors) | free | | Read one | [`GET /monitors/{id}`](/docs/api-reference/monitors/getMonitor) | free | | Change types or metadata, or pause | [`PATCH /monitors/{id}`](/docs/api-reference/monitors/updateMonitor) | free | | Delete | [`DELETE /monitors/{id}`](/docs/api-reference/monitors/deleteMonitor) | free | Only creating a monitor costs anything. Managing your own is free, so there is no reason to cache the list rather than reading it. `PATCH` changes only what you send: `{"status": "paused"}` stops delivery without touching the event types, and `{"status": "active"}` resumes it. A paused monitor keeps its configuration and delivers nothing. A monitor counts against the number of companies your plan monitors at once while it is active; a paused one does not. Resuming a monitor created in the dashboard when that number is reached answers `403` `monitor_limit_reached`, the refusal a create gets at the same limit, and the monitor stays paused. Monitors created with an API key or through the MCP server never count. A monitor belonging to another account answers `404`, not `403` — a `403` would confirm it exists. ## Receiving events [#receiving-events] Events are sent to the active destinations on your account — e-mail addresses, webhook URLs and Slack channels (by the incoming-webhook URL Slack issues), set up in the app under [Benachrichtigungen](https://www.registercheck.de/alerts#benachrichtigungen) or with [`POST /destinations`](/docs/api-reference/destinations/createDestination). A destination you set up yourself receives every event of every monitor; [Destination roles](#destination-roles) names the two kinds that receive less. A destination's `cadence` is `instant` (every change on its own), or a `daily`, `weekly` or `monthly` digest sent at 07:00 Europe/Berlin; a period with no changes sends nothing. Each send is a delivery: [`GET /deliveries`](/docs/api-reference/deliveries/listDeliveries) lists what was sent where, and [`GET /events?monitor_id=`](/docs/api-reference/events/listEvents) lists what a monitor matched, whether or not it was sent. An event a monitor reports carries the moment the monitor reported it as its `created_at`: on [`GET /events?monitor_id=`](/docs/api-reference/events/listEvents), in each delivery and in the webhook body, the same instant in all three. A monitor created in the dashboard reports a change at its next check (see [Detection latency](#detection-latency)), so its `created_at` can be later than the moment the change reached Registercheck. Polling a monitor's events on `created_at[gte]` still finds every one, whenever it is reported; the list stays ordered by the register day first. Each destination has a `cadence`: `instant` sends every change on its own as it is detected; `daily`, `weekly` and `monthly` send one digest of the period's changes at 07:00 Europe/Berlin — every day, every Monday, on the 1st. A new e-mail address or Slack channel is `daily` and a webhook `instant` unless you say otherwise; a destination you set up before cadences existed stays `instant`. Change it with [`PATCH /destinations/{id}`](/docs/api-reference/destinations/updateDestination). A period without changes sends nothing. Switching to a digest starts its first period then — it never collects what you were already told — and switching back to `instant` sends the open period at once. A webhook is told at once for now. For Slack, create an incoming webhook for the channel in Slack and send its URL as `address` with `"channel": "slack"`. Each change arrives as a short German message: what changed, the register date and a link to the company. The URL is Slack's key to that channel, so it is shown in full only when you create the destination; afterwards it reads `https://hooks.slack.com/services/T…/B…/…wxyz`. If Slack keeps refusing the webhook (it was removed, or the channel archived), the destination is switched off after ten failed deliveries in a row, as a webhook is; `disabled_reason` says so. A webhook destination should: * Respond `2xx` quickly and do the work afterwards. Slow handlers are retried. * **Be idempotent.** Assume every delivery may arrive more than once, and deduplicate on the `svix-id` header (the delivery's id) and on each event's `id`. * Treat the payload as a signal, not as truth. Re-read the company to get the new state. [Webhooks](/docs/concepts/webhooks) has the exact payload, the signature headers and a verified handler. ### Destination roles [#destination-roles] Every destination carries a `role`, which says what it receives: | `role` | What it is | What it receives | | --------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `own` | A destination you set up, here or in the app | Every event of every monitor, on its cadence | | `account_inbox` | Your sign-in address, added by Registercheck when you created your first monitor in the dashboard or through the MCP server | The events of monitors created with an API key, as before; a dashboard monitor's only when that monitor delivers to it | | `route` | An e-mail address, Slack channel or webhook a dashboard monitor delivers to | Only the events of the dashboard monitors that deliver to it, at their check times | A `route` destination is always `instant`: a request to change its cadence answers `409` with code `route_endpoint`. Creating a destination with the address of a `route` makes that destination `own` instead of answering `409`, and from then on it receives every event. A dashboard monitor that delivers to one of your `own` destinations sends its events there at its check time, and that destination's digest leaves them out, so nothing arrives twice. ## Detection latency [#detection-latency] Changes are detected by re-reading register entries every 15 minutes, not by a push from the courts. A change is typically visible within a day or two of appearing in the register, which is itself days or weeks after the underlying event. Monitoring is for keeping a portfolio current, not for real-time decisions. A monitor created with an API key reports a change as soon as it is detected. A monitor created in the dashboard reports at its check (*Prüfung*). The plan sets how often a monitor may check at most: every 15 minutes on the Scale plan, hourly on Business and the earlier paid plans, otherwise daily at 06:00 Europe/Berlin. Each monitor checks at the speed chosen for it — weekly on Monday at 06:00 is always possible — or, without a choice, at the speed of your account inbox's cadence: `instant` the plan's fastest, `daily` daily, `weekly` and `monthly` weekly. Changing the inbox's cadence with [`PATCH /destinations/{id}`](/docs/api-reference/destinations/updateDestination) therefore changes how often those monitors check. # Ownership (/docs/concepts/ownership) ## What the register holds [#what-the-register-holds] Germany has no public register of beneficial ownership you can simply read. Ownership comes from the **shareholder list** (*Gesellschafterliste*), which a GmbH must file whenever its shareholders change. This has three consequences you must design around: 1. **It is a document, not a field.** The list is a PDF. What the API returns is what was extracted from it. 2. **It is only as fresh as the last filing.** A company whose shareholders last changed in 2014 has a 2014 list, and that is correct. 3. **Not every legal form has one.** An `AG` files no shareholder list; a sole trader (`e.K.`) has no shareholders. An empty response is frequently the right answer. ## Reading ownership [#reading-ownership] [`GET /companies/{company_id}/shareholders`](/docs/api-reference/companies/listCompanyShareholders) returns who holds an interest in the company: shareholders, and for partnerships the general and limited partners. Each one is a `shareholding`: ```json { "object": "page", "data": [ { "object": "shareholding", "id": "shr_01h455vb4pex5vsknk084sn02q", "company": { "object": "company", "id": "cmp_01h455vb4pex5vsknk084sn02r", "name": "Beispiel Analytics GmbH" }, "holder": { "object": "company", "id": "cmp_01h455vb4pex5vsknk084sn02s", "name": "Beispiel Beteiligungs GmbH" }, "type": "shareholder", "percentage": 40, "nominal_amount": { "amount": "10000.00", "currency": "EUR" }, "liability_amount": null, "match_confidence": 1, "source": { "object": "document", "id": "doc_01h455vb4pex5vsknk084sn02t", "date": "2025-06-03", "type": "shareholder_list" } } ], "next_cursor": null, "extract": { "status": "parsed", "fetched_at": "2025-06-10T08:12:44Z", "document_id": "doc_01h455vb4pex5vsknk084sn02t" } } ``` `type` says what the interest is: `shareholder`, `general_partner` (*Komplementär*) or `limited_partner` (*Kommanditist*). `source` is the filing it was read from, and `source.date` is the date of that filing. **Always show it.** A percentage without its as-of date is a claim you cannot support. `holder` is a party: `object` says whether the holder is a company or a person, and `id` leads to its record. `id` is `null` when the register names a holder we could not match to a record, such as a company registered abroad; `name` is then the name as filed. ### Holders that are companies [#holders-that-are-companies] Where a holder is itself a company, [`GET /companies/{company_id}/ownership-tree`](/docs/api-reference/companies/getOwnershipTree) follows linked companies for the `depth` you ask, from 1 to 3. Each node is a `shareholding` with its `depth`, its `effective_percentage` of the company you asked about (the product of the percentages along the path), and `via_shareholding_id`, the holding one level down. The chain stops, and says why, when it reaches a company we have not linked (`foreign_entity` when that company is registered abroad, `entity_not_linked` when the country is Germany or not stated), an AG, SE or KGaA (`stock_corporation`; those forms file no shareholder list — a partnership worded "AG & Co. KG", "AG & Co. OHG", "AG & Co. Kommanditgesellschaft" or "SE & Co. Offene Handelsgesellschaft" is not one of them), a company whose list is not stored yet (`no_shareholder_list` or `extraction_pending`), a percentage that cannot be published (`percentage_withheld`), the `depth` you asked for (`depth_limit`), a cycle (`cycle`) or a company that holds itself (`self_holding`). Those holdings are listed in `unresolved`, once per reason when the same holding is reached by several paths. `complete` is true only when that list is empty, the walk was not cut short, and `list_reason` is null. `truncated` is true when `depth` or the cap of 250 nodes stopped it. At depth 1, `effective_percentage` is the holding's own percentage. Further down it is the product of the percentages along the path, rounded half up to four decimal places. An empty `nodes` does not by itself say whether no shareholder list is on file or whether it is not extracted yet. `list_reason` is `no_shareholder_list` or `extraction_pending` when that list is missing or still being read, including when partners are listed, and `complete` is false. The shareholder list still says which (`extract.status`). Asking starts at most one shareholder extraction, for the company you asked about. Companies further up are read from ownership already on file. A person with a Sperrung is left out at every level. The call costs 10 credits whatever `depth` you ask. The percentages are computed from filed shareholder lists. They are not an extract from the Transparenzregister and not a determination under § 3 GwG. ## Computed beneficial owners [#computed-beneficial-owners] [`GET /companies/{company_id}/beneficial-owners`](/docs/api-reference/companies/getComputedBeneficialOwners) is a separate read. It follows the same filings for five levels and lists each natural person the computation qualifies. A company is never a beneficial owner. Each person carries `percentage`, `gwg_above_25`, `amlr_at_least_25`, `basis` and `paths`: the shareholdings from this company out to that person, and the percentage multiplied along each path. Two holdings are the same person when exactly one person record is linked (`matched_by` is `person_id`), or, failing that, when both give the same full name and the same birth date (`matched_by` is `name_birth_date`). Otherwise the holding stays on its own (`matched_by` is `unidentified`), and its direct and indirect stakes are not combined with another row. `unresolved` includes `unlinked_person` when that unidentified person's largest possible stake is still at least 25% of the capital, or their control stake is 25% or more, or when the row has the same name as an identified holder and the two stakes together could reach that line. Those rows are not combined into one person. A smaller unidentified stake, which could not reach the line even together with such a holder, does not make the status `partial`. `gwg_above_25` is the GwG capital test: the paths sum to more than 25%. `amlr_at_least_25` is the AMLR test: the paths sum to 25% or more. Exactly 25% sets the AMLR flag and not the GwG flag. `basis` repeats those tests and adds control: | Value | Meaning | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `capital_over_25` | The paths sum to more than 25% of the capital. Same test as `gwg_above_25`. | | `control_chain` | The person's own stake in this company, plus the stakes of companies they control, is more than 25%. Control of another company is a majority, more than half, and stakes of several controlled companies in one company are added. | | `amlr_25_or_more` | The paths sum to 25% or more. Same test as `amlr_at_least_25`. It does not by itself clear the fictitious rule. | `status` is `complete`, `partial` or `none_found`. `unresolved` always includes `voting_rights_unknown`, because a shareholder list does not state votes; that reason alone does not make the status `partial`. Other reasons do: a missing percentage, a holder withheld by a Sperrung (`withheld_person`), an unidentified person who could still reach 25% (`unlinked_person`), a holder abroad or not linked, an AG, SE or KGaA, no shareholder list, a list still being read, a partnership whose figure is only a Haftsumme proxy, the five-level limit, the cap of 250 nodes, a cycle, or a company that holds itself. `fictitious_rule_applies` follows the GwG test. It is true when the look is complete and nobody has `capital_over_25` or `control_chain`, including when the only qualification is the AMLR test. It is false when the status is `partial`, so a withheld person is not reported as nobody. The managing directors are not copied into `beneficial_owners`; they stay on [`GET /companies/{company_id}/roles`](/docs/api-reference/companies/listCompanyRoles). `notice` is the same sentence on every answer: computed from register filings (shareholder lists and register entries); may be incomplete. It is not an extract from the Transparenzregister, not the beneficial owners reported under §§ 19–20 GwG, and no substitute for the due diligence a Verpflichteter must do. In German: rechnerisch aus Gesellschafterlisten und Registereinträgen ermittelt; kann unvollständig sein. Kein Auszug aus dem Transparenzregister, nicht die nach §§ 19–20 GwG gemeldeten wirtschaftlich Berechtigten und kein Ersatz für die eigenen Sorgfaltspflichten eines Verpflichteten. A person with a Sperrung is left out. The call costs 25 credits. The reverse direction — [`GET /companies/{company_id}/holdings`](/docs/api-reference/companies/listCompanyHoldings) and [`GET /persons/{person_id}/holdings`](/docs/api-reference/persons/listPersonHoldings) — returns the shareholdings a company or a person holds in other companies. They are the same `shareholding` objects, seen from the holder's side. German group structures contain cycles, including a company that holds itself. The tree flags the holding that would close one and does not follow it. If you walk the chain yourself, cap the depth and keep a visited set per path, so a company reached two ways is counted twice. ## When we hold no list yet [#when-we-hold-no-list-yet] Asking for shareholders starts the work by itself. If we hold no shareholder list for this company, the page says `extract.status: "processing"` with no holders, and the same request a little later returns them. One call starts at most one extraction, for the company you asked about. That is why `extract` matters more than an empty `data` array: | `extract.status` | Meaning | | ---------------- | ----------------------------------------------------- | | `parsed` | What you see is what is on file, as of `fetched_at`. | | `processing` | A list is being read now. Ask again in a few minutes. | | `not_filed` | No list exists to read. The empty page is the answer. | | `not_fetched` | We have not fetched the list from the register. | | `failed` | We fetched the list and could not read it. | Re-reading the *history* of past lists is planned (see the [roadmap](/docs/roadmap)). Either way, a rebuild re-reads filed documents; it cannot invent a list that was never filed. ## What you cannot get [#what-you-cannot-get] * **The Transparenzregister, or an official determination.** Neither is in this API. The ownership tree's effective percentages, and [`GET /companies/{company_id}/beneficial-owners`](/docs/api-reference/companies/getComputedBeneficialOwners), are computed from filed shareholder lists and register entries. They may be incomplete. * **Share transfers between filings.** Only the filed snapshots exist. * **Shareholdings in an AG.** Not filed, therefore not available. # Persons (/docs/concepts/people) ## Person records [#person-records] [`GET /persons/{person_id}`](/docs/api-reference/persons/getPerson) returns a natural person named in the register: ```json { "object": "person", "id": "per_01hxn3rys1fh58w4d1pb1x9s82", "name": { "full": "Erika Mustermann", "first": "Erika", "last": "Mustermann", "birth_name": null, "titles": [] }, "birth_date": "1970-01-01", "birth_year": 1970, "birth_month": 1, "age": 56, "address": { "city": "Musterstadt", "country": "DE" } } ``` The `age` in that example is as of 8 October 2026. What the person does at companies is listed separately, from the person's side: * [`GET /persons/{person_id}/roles`](/docs/api-reference/persons/listPersonRoles) — the offices they hold or held: managing director, board member, Prokurist, liquidator. Narrow it with `role_type`, or add `expand=roles` to the person request to get the first page inline. * [`GET /persons/{person_id}/holdings`](/docs/api-reference/persons/listPersonHoldings) — the interests they hold: shares, and Kommanditist interests in limited partnerships. The same `role` and `shareholding` objects are listed from the company's side, at `/companies/{company_id}/roles` and `/companies/{company_id}/shareholders`. Each names the company and the holder as a party, so you can follow either direction. A role or shareholding links a holder named in a register entry to a person record. `match_confidence` is how sure that link is, from 0 to 1. It is not a probability and not proof that two records are the same person. A lower score does not necessarily mean the birth day was missing. Confirm the name, the birth date and the city before you treat two records as one person. The score is `null` when the holder is a company or could not be linked. Surface it whenever you show the link. ## How the score is assigned [#how-the-score-is-assigned] The API does not compute `match_confidence`. It publishes a score stored when a register profile was linked to a person record. Person endpoints, [`GET /companies/{company_id}/roles`](/docs/api-reference/companies/listCompanyRoles) and a company's history publish that stored number only when it is a number from 0 to 1. A value outside that range is left unpublished, and `match_confidence` is `null`. A company role is linked only when the register profile is visible and every stored match of that profile is a person who may be shown. If any match is a restricted person, no link is published: the holder has no person id and `match_confidence` is `null`. The API does not publish another match of that profile instead. When every match may be shown, the published score is the highest stored score for that profile. More than one matcher has written that stored score, and they do not share one scale. A number is therefore not one level with one meaning across the whole dataset. The batch function that wrote many of the rows stores only the scores below. A dated profile is linked to a person with the same birth date or the same birth year, or a new person is created. A profile with no birth date is the only case that stores 0.50. | Score | What that function required | | ----- | ------------------------------------------------------------------------------------------------ | | 0.95 | The normalized full name and the full birth date, including a person just created with that date | | 0.90 | That name with first and last swapped, and the full birth date | | 0.75 | The normalized full name and the birth year. The day may differ. | | 0.70 | The swapped name and the birth year | | 0.50 | The profile has no birth date | Other jobs write other numbers into the same field: * Reading a managing director, a Prokurist or a limited partner stores **0.80** with no further test. * The matching container's batch script uses those same 0.95, 0.90 and 0.75 rules. It also stores **0.70** when only the last name and the full birth date agree, and **0.50** when the name matches and the birth date differs, or the match was by name alone. * An older batch script is retired: nothing in the repository calls it. Rows it already wrote can still be published. It stored **0.95** for the name and the full birth date, **0.90** for the swapped name and the full birth date, **0.75** for the name and the birth year, **0.70** for the last name and the full birth date, **0.80** for the swapped name and the birth year, a name-only score of at most **0.50** that gets smaller as more people share the name, and **0.0** when no rule matched. * Another writer starts from **0.90** and divides it by how many person records the profile is linked to. Two equal scores can therefore come from different rules. ## What to do with the score [#what-to-do-with-the-score] * **Show** the score whenever you show the link. A published value is from 0 to 1, or the field is `null`. * **Do not auto-merge** on the score. No matcher treats a value as proof that two records are one person. Confirm the name, the birth date and the city. * **Never** merge a holder whose score is `null`. That holder is a company, no person was linked, or any match of the profile is a restricted person. On the batch function, 0.50 means the profile had no birth date. The day may differ at 0.75, a same-year match. ## How reliable is `match_confidence` [#how-reliable-is-match_confidence] Measured on 2026-10-14. **Precision** is the share of links whose register entry and person record read as the same person, among the links the register can decide. **Undecidable** is the share the register cannot decide, mostly entries without a birth date. | Score | n | Precision | 95 % interval | Undecidable | | ----- | --- | --------- | ------------- | ----------- | | 1.00 | 78 | 100 % | 95.4–100 % | 0 % | | 0.95 | 300 | 100 % | 98.8–100 % | 0 % | | 0.90 | 300 | 100 % | 98.8–100 % | 0.3 % | | 0.80 | 300 | 100 % | 98.7–100 % | 7.7 % | | 0.75 | 150 | 2.0 % | 0.4–5.8 % | 0.7 % | | 0.70 | 3 | 0 % | 0–70.8 % | 0 % | | 0.60 | 5 | 40 % | 5.3–85.3 % | 0 % | | 0.50 | 300 | 96.3 % | 92.1–98.6 % | 46.3 % | The undecidable links at 0.80 are entries without a birth date whose person was made from that one entry: such a link cannot join two people. At 0.50 the entry has no birth date and nothing else in the register ties it to the person. **Recommendation:** use links with `match_confidence` ≥ 0.80; below that, treat the person link as a suggestion. In the sample, none of the 954 decidable links at 0.80 or above read as a different person. **Corrected on 2026-10-15.** The 0.75, 0.70 and 0.60 rows above were measured before a correction: 605 links stored at those scores had a register entry that contradicted the linked person (another birth date or another last name). Each now points to the person with the entry's own name and birth date (0.95), to the person with first and last name swapped and that birth date (0.90), or to a new person made from the entry (0.95). What this means for you: * No link is published at 0.75 or 0.70 any more. At 0.60, only links whose name and birth date agree remain. * A role or shareholding of a corrected entry can now name another person id than before. A person id you stored for one of them may no longer list that role. Re-read the roles and shareholdings you cached before 2026-10-15. **Method.** A random sample of 1,436 published links, drawn per score (every link at 1.00, 0.70 and 0.60), was read in two independent, automated passes that compared each register entry with its person record without seeing the score; the passes agreed on 97.8 % (Cohen's κ 0.941), and a third reading settled the 32 disagreements. Precision is same / (same + different) per score, with a 95 % Clopper-Pearson interval. Two people with the same name and the same birth date cannot be told apart in the register, so no measurement here can see them. The readers saw the register as Registercheck stores it, not the original documents. ## Matching is name-based, and names are not unique [#matching-is-name-based-and-names-are-not-unique] A person here is "a named holder", assembled from register entries. Where the register gives a city and a date of birth, matching is reliable. Where it gives only a name — which is common in older entries — it is not. Design for this: * **Do not treat a person id as an identity.** Two `Rolf Müller` entries may be one human or two. * **Check `address.city`, `birth_date` and `age` before merging** a person into your own records. `age` is whole years as of today in Europe/Berlin, computed from `birth_date` on each request and never stored. It is `null` when the date is missing, in the future, or the age would be over 120. * **Expect duplicates** for common names, and expect a single human to appear under several ids across courts. ## Privacy [#privacy] Register data about natural persons is personal data. Some people have exercised their right to object, and their records are restricted: search, person records, roles and shareholdings never return them. A restriction is applied after the fact, so a person you fetched last month may have been removed since. Re-read anything you have cached before displaying it again, and delete records that no longer come back. Personal data from this API is subject to the GDPR in your hands as well as ours. A lawful basis for processing it is yours to establish. # Webhooks (/docs/concepts/webhooks) A monitor is only half of monitoring. The other half is where we send what it finds: a **destination**. This page is the contract for webhook destinations: how to set one up, what arrives, how to verify it, and what you must not assume. ## Set up a destination [#set-up-a-destination] Destinations belong to your account, not to a single monitor: **every event on every one of your monitors is sent to every active destination you set up yourself** (its `role` is `own`, see [Destination roles](/docs/concepts/monitoring#destination-roles)). Add one in the app under [Benachrichtigungen](https://www.registercheck.de/alerts#benachrichtigungen), or through the API with [`POST /destinations`](/docs/api-reference/destinations/createDestination) and `"channel": "webhook"`, with your URL as `address`. The same place takes e-mail addresses and Slack channels. The response to creating a webhook destination carries its **signing secret** (`whsec_…`). It is shown this once and never again: store it, you need it to verify deliveries, and it is the only thing separating a real delivery from anyone who learns your URL. Lost it? Disable the destination and create a new one. Disabling ([`DELETE`](/docs/api-reference/destinations/deleteDestination)) keeps the destination listed so you can turn it back on ([`PATCH`](/docs/api-reference/destinations/updateDestination) with `{"disabled": false}`). ## What arrives [#what-arrives] A **delivery**, sent as `Content-Type: application/json`: the same object [`GET /deliveries/{delivery_id}`](/docs/api-reference/deliveries/getDelivery) lists, with the events it carries. An `instant` destination gets one delivery per event; a daily, weekly or monthly destination gets one per period, holding that period's events. Each event is the same object [`GET /events?monitor_id=`](/docs/api-reference/events/listEvents) lists, so one parser serves all three. The body is exactly what was signed: ```json { "object": "delivery", "id": "dlv_01k6yq3m0nfx8a4q2t7bw9c5ve", "destination_id": "dst_6hq30w96je9rnrmw1tknzhwb2n", "cadence": "instant", "status": "pending", "attempt_count": 0, "events": [ { "object": "event", "id": "evt_1c9sn8p79z99e9wyrf38nkrkay", "type": "company.registered_office.changed", "subject": { "object": "company", "id": "cmp_1z3gn9wpvx9j59w8bd9w5rm72n", "name": "Beispiel Analytics GmbH" }, "data": { "previous": "München", "current": "Augsburg" }, "source": null, "effective_date": "2026-09-21", "created_at": "2026-09-21T10:14:02Z" } ], "created_at": "2026-09-21T10:14:05Z", "sent_at": null } ``` | Field | Notes | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | The delivery's id. It is also the `svix-id` header, and a retry carries it again: deduplicate on it. | | `cadence` | The destination's cadence when the delivery was made: `instant` for one event, else the digest's. | | `status` / `attempt_count` | Where the delivery stood when this send was made: `pending`, and how many sends were tried before. [`GET /deliveries`](/docs/api-reference/deliveries/listDeliveries) shows how it ended. | | `events[].type` | The event type your monitor subscribed to, e.g. `company.role.added`. Labels in English and German are on [`GET /event-types`](/docs/api-reference/reference-data/listEventTypes). | | `events[].subject.id` | The company to re-read. This is the field you actually act on. | | `events[].data` | What changed, shaped by the type: `previous` / `current` for a changed value, the holder and `role_type` for a role. Values from the register are verbatim, and some are a sentence of register text, so do not assume they are short. | | `events[].effective_date` | For a register entry, the date the register recorded it — **not** when we detected it, so an event about an old entry can arrive today. For shareholder lists and annual accounts, the day we first read them, since neither carries a register date. `null` when no date is known. | One change covered by two of your monitors is one event: its `id` is the same wherever it is listed. An event naming a company or person taken off the platform since it was decided is left out, and a delivery with no event left is not sent. A dashboard monitor can deliver to a webhook of its own (a destination with `role` `route`). That webhook receives only the events of the dashboard monitors that deliver to it, each as its own delivery at the monitor's check time, listed by [`GET /deliveries`](/docs/api-reference/deliveries/listDeliveries) with `cadence` `instant`. The e-mail and Slack summaries of dashboard monitors are not API deliveries and are not listed. ## Saved searches [#saved-searches] A saved search on the [Ereignisse](https://www.registercheck.de/events) page can send what it finds to a webhook of its own. Every hit arrives as a request of its own, signed, retried and deduplicated as described below, with this body: ```json { "object": "search_delivery", "id": "5d3b1f0c-8e2a-5c41-9b7d-0f6e4a2c8d19", "test": false, "search": { "object": "event_search", "id": "8c3f0e2a-1b4d-4c6e-9f80-123456789abc", "name": "Insolvenzen Bayern" }, "change": { "id": "3f1c2a9e-5b7d-4c8a-9e21-6d4f0b8a1c55:14:insolvency:2026-10-14", "kind": "insolvency", "types": ["company.insolvency.recorded"], "effective_date": "2026-10-14", "created_at": "2026-10-14T09:12:03.284117Z", "previous": null, "current": null, "text": "Über das Vermögen der Gesellschaft ist das Insolvenzverfahren eröffnet. Die Gesellschaft ist aufgelöst.", "roles": [], "company": { "object": "company", "id": "cmp_1z3gn9wpvx9j59w8bd9w5rm72n", "name": "Beispiel Bau GmbH", "legal_form": "gmbh", "register": { "court": "Amtsgericht München", "type": "HRB", "number": "123456", "formatted": "Amtsgericht München HRB 123456" }, "registered_office": "München", "state": "DE-BY" } }, "credits": 1, "created_at": "2026-10-14T09:13:00.418203Z" } ``` | Field | Notes | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `id` | The request's id, an opaque string, also its `svix-id` header. A retry carries it again: deduplicate on it. | | `test` | `true` for "Test senden" in the app: an invented company, no register data, no credits. | | `search` | The saved search that found the change, as the app names it. Its `id` is an opaque string. | | `change` | One register entry's change of one kind, as the Ereignisse list shows it. Its `id` is an opaque string: compare it, never parse it. | | `change.types` | The event types the change records, as [`GET /event-types`](/docs/api-reference/reference-data/listEventTypes) lists them. A `management` or `prokura` change carries `company.role.added`, `company.role.changed` or `company.role.removed`, and so does a `liquidation` that names its liquidator, beside `company.liquidation.recorded` when the entry also records the dissolution. Other kinds carry their own type, e.g. `company.insolvency.recorded`. | | `change.effective_date` | The date the register recorded the entry, **not** when we detected it, so a hit about an old entry can arrive today. | | `change.created_at` | When we first recorded the change, in UTC, with a fraction of a second when there is one. | | `change.previous` / `current` / `text` | Register text, verbatim, or `null`. `previous` and `current` are the old and new value for `registered_office`, `name`, `purpose` and `capital`; `text` is the entry's own wording for `articles`, `conversion`, `liquidation`, `insolvency` and `deregistration`, and for a seat move that the register wrote only in words. `management` and `prokura` name their people in `roles`, and a `liquidation` names its liquidator there too, beside its `text`. | | `change.roles` | The people or companies the entry names for a role, each with `object` (`person` or `company`), `name`, `role_type` and `change` (`appointed`, `changed`, `removed`). `role_type` is one of the codes [`GET /role-types`](/docs/api-reference/reference-data/listRoleTypes) lists, read from the function the entry names: "Geschäftsführer" is `managing_director`, "Liquidator" `liquidator`, a Prokura `authorized_signatory`. An entry that names no function is `representative`; a function our rules do not recognise is `other`, as is the rare role stored without a reading. No birth dates. | | `change.company.id` | The company to re-read through the API: the one API id in this body (`cmp_…`). | | `credits` | What the hit cost: a saved search costs 1 credit per hit, charged when the hit is found, or when you unlock a hit that a paused search missed. A hit that could not be delivered is refunded. | | `created_at` | When the hit was handed over for sending, in UTC, with a fraction of a second when there is one. For a hit it is the same on every retry, and so is the whole body. | These requests are not monitor deliveries: [`GET /deliveries`](/docs/api-reference/deliveries/listDeliveries) does not list them, and they go only to the webhook the saved search names. If every attempt at a hit fails, the hit is refunded, and the change stays on the saved search's tab on the Ereignisse page: after an outage, catch up there. The app shows a new webhook's signing secret (`whsec_…`) once, when you save the search or change where it delivers; a URL you already receive deliveries at keeps its secret. ## Verifying a delivery [#verifying-a-delivery] Deliveries follow the [Standard Webhooks](https://www.standardwebhooks.com/) specification. Three headers come with every request: | Header | Meaning | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `svix-id` | The delivery's `id`. A retry carries the same id. | | `svix-timestamp` | When this attempt was signed, in Unix seconds. Reject old ones to stop replays. | | `svix-signature` | `v1,`: HMAC-SHA256 over `{svix-id}.{svix-timestamp}.{raw body}`, keyed with your secret base64-decoded after its `whsec_` prefix. | The header names are the ones Svix uses, so any Standard Webhooks library verifies them. Use one rather than hand-rolling it — it does the constant-time comparison and the timestamp tolerance for you: ```typescript import { Webhook } from 'svix'; // or the `standardwebhooks` package const wh = new Webhook(process.env.REGISTERCHECK_WEBHOOK_SECRET!); // the whsec_… secret app.post('/hooks/registercheck', express.raw({ type: 'application/json' }), (req, res) => { let delivery; try { delivery = wh.verify(req.body, { // the RAW body, not the parsed object 'svix-id': req.headers['svix-id'] as string, 'svix-timestamp': req.headers['svix-timestamp'] as string, 'svix-signature': req.headers['svix-signature'] as string, }); } catch { return res.status(400).end(); // unverified: do not process } res.status(200).end(); // acknowledge, then work void handle(delivery); // delivery.id === the svix-id header }); ``` Signature verification needs the **raw** request body. A JSON body-parser mounted earlier will re-serialise it and the signature will not match. ## Retries, duplicates and order [#retries-duplicates-and-order] * **Answer `2xx` within 30 seconds.** A `5xx`, a `429`, no answer in time or no connection is retried: six attempts in all, with randomised backoff, over roughly a quarter of an hour. Any other `4xx` is final. Reject with `4xx` only what you never want again (a bad signature), and answer `5xx` or `429` when you are merely busy. * **A change is never lost with its delivery.** If every attempt fails, the delivery is listed by [`GET /deliveries`](/docs/api-reference/deliveries/listDeliveries) as `failed`, and its events stay listed by [`GET /events?monitor_id=`](/docs/api-reference/events/listEvents). After an outage longer than the retry window, read them to catch up. * **Deduplicate on the delivery `id`** (the `svix-id` header). Delivery is at least once, and a retry carries the same id. Within a delivery, an event's `id` is the same one every list shows. * **Do not rely on order.** For register entries, `effective_date` is the register's own chronology. * **A failing destination is switched off.** After ten failed deliveries in a row we disable it and say why (`disabled_reason` on the destination). Fix the receiver, then turn it back on. ## Treat the payload as a signal [#treat-the-payload-as-a-signal] An event's `data` tells you what the register said at detection time. It is not a substitute for the company record, and it may no longer be current when you process it. Re-read the company and act on that: ``` delivery arrives → verify → dedupe on delivery.id → for each event: GET /companies/{event.subject.id} → apply ``` The [track changes guide](/docs/guides/track-changes) walks through this end to end. # Retrieve your account (/docs/api-reference/account/getAccount) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Overview (/docs/api-reference/account) The workspace your key acts in, its plan, the credits it has left and your monitor usage. Free, and answered even when the credits are gone. | Endpoint | Route | Cost | | --------------------------------------------------------------- | -------------- | ---- | | [Retrieve your account](/docs/api-reference/account/getAccount) | `GET /account` | free | # Retrieve a company (/docs/api-reference/companies/getCompany) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Compute beneficial owners from register filings (/docs/api-reference/companies/getComputedBeneficialOwners) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Assemble a KYB report for a company (/docs/api-reference/companies/getKybReport) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve the tree of a company's holders (/docs/api-reference/companies/getOwnershipTree) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Overview (/docs/api-reference/companies) The register entry itself — name, registered office, purpose, capital, legal form — and everything listed under it: roles, shareholders, holdings, documents, financial statements and the events of one company. The KYB report assembles the register entry, roles, shareholders and source filings for one request. | Endpoint | Route | Cost | | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------- | ---------- | | [Retrieve a company](/docs/api-reference/companies/getCompany) | `GET /companies/{company_id}` | 10 credits | | [List the roles at a company](/docs/api-reference/companies/listCompanyRoles) | `GET /companies/{company_id}/roles` | 10 credits | | [List a company's register history](/docs/api-reference/companies/listCompanyEvents) | `GET /companies/{company_id}/events` | 10 credits | | [List who holds an interest in a company](/docs/api-reference/companies/listCompanyShareholders) | `GET /companies/{company_id}/shareholders` | 10 credits | | [List the companies a company holds interests in](/docs/api-reference/companies/listCompanyHoldings) | `GET /companies/{company_id}/holdings` | 10 credits | | [Retrieve the tree of a company's holders](/docs/api-reference/companies/getOwnershipTree) | `GET /companies/{company_id}/ownership-tree` | 10 credits | | [Compute beneficial owners from register filings](/docs/api-reference/companies/getComputedBeneficialOwners) | `GET /companies/{company_id}/beneficial-owners` | 25 credits | | [Assemble a KYB report for a company](/docs/api-reference/companies/getKybReport) | `GET /companies/{company_id}/kyb-report` | 50 credits | | [List a company's register documents](/docs/api-reference/companies/listCompanyDocuments) | `GET /companies/{company_id}/documents` | 1 credit | | [List a company's financial statements](/docs/api-reference/companies/listCompanyFinancialStatements) | `GET /companies/{company_id}/financial-statements` | 10 credits | # List a company's register documents (/docs/api-reference/companies/listCompanyDocuments) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List a company's register history (/docs/api-reference/companies/listCompanyEvents) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List a company's financial statements (/docs/api-reference/companies/listCompanyFinancialStatements) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List the companies a company holds interests in (/docs/api-reference/companies/listCompanyHoldings) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List the roles at a company (/docs/api-reference/companies/listCompanyRoles) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List who holds an interest in a company (/docs/api-reference/companies/listCompanyShareholders) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve a delivery (/docs/api-reference/deliveries/getDelivery) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Overview (/docs/api-reference/deliveries) What was sent to which destination, and the webhook a delivery arrives as. Free. | Endpoint | Route | Cost | | ----------------------------------------------------------------- | ------------------------------- | ---- | | [List deliveries](/docs/api-reference/deliveries/listDeliveries) | `GET /deliveries` | free | | [Retrieve a delivery](/docs/api-reference/deliveries/getDelivery) | `GET /deliveries/{delivery_id}` | free | | [Delivery](/docs/api-reference/deliveries/receiveDelivery) | webhook `delivery` | — | # List deliveries (/docs/api-reference/deliveries/listDeliveries) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delivery (/docs/api-reference/deliveries/receiveDelivery) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create a destination (/docs/api-reference/destinations/createDestination) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete a destination (/docs/api-reference/destinations/deleteDestination) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve a destination (/docs/api-reference/destinations/getDestination) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Overview (/docs/api-reference/destinations) Where your monitors' events are sent: e-mail addresses and webhook URLs. Free. | Endpoint | Route | Cost | | -------------------------------------------------------------------------- | --------------------------------------- | ---- | | [List destinations](/docs/api-reference/destinations/listDestinations) | `GET /destinations` | free | | [Create a destination](/docs/api-reference/destinations/createDestination) | `POST /destinations` | free | | [Retrieve a destination](/docs/api-reference/destinations/getDestination) | `GET /destinations/{destination_id}` | free | | [Update a destination](/docs/api-reference/destinations/updateDestination) | `PATCH /destinations/{destination_id}` | free | | [Delete a destination](/docs/api-reference/destinations/deleteDestination) | `DELETE /destinations/{destination_id}` | free | # List destinations (/docs/api-reference/destinations/listDestinations) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Update a destination (/docs/api-reference/destinations/updateDestination) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve a register event (/docs/api-reference/events/getCompanyEvent) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve a register event (/docs/api-reference/events/getEvent) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Overview (/docs/api-reference/events) Every change the register courts enter, across all companies, newest register day first. Follow them by polling on `created_at[gte]`. | Endpoint | Route | Cost | | --------------------------------------------------------------------------------------- | ---------------------------------------- | ---------- | | [List events](/docs/api-reference/events/listEvents) | `GET /events` | 10 credits | | [Retrieve a register event](/docs/api-reference/events/getEvent) | `GET /events/{event_id}` | 1 credit | | [List events (deprecated)](/docs/api-reference/events/listCompanyEventsAcrossCompanies) | `GET /company-events` | 10 credits | | [Retrieve a register event (deprecated)](/docs/api-reference/events/getCompanyEvent) | `GET /company-events/{company_event_id}` | 1 credit | # List events (/docs/api-reference/events/listCompanyEventsAcrossCompanies) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List events (/docs/api-reference/events/listEvents) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve a formation (/docs/api-reference/formations/getFormation) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Overview (/docs/api-reference/formations) Companies whose first register entry is a new registration, newest register day first. The metadata you add to one stays private to your workspace. | Endpoint | Route | Cost | | ------------------------------------------------------------------------------- | ---------------------------------- | ---------- | | [List new formations](/docs/api-reference/formations/listFormations) | `GET /formations` | 10 credits | | [Retrieve a formation](/docs/api-reference/formations/getFormation) | `GET /formations/{formation_id}` | 1 credit | | [Update a formation's metadata](/docs/api-reference/formations/updateFormation) | `PATCH /formations/{formation_id}` | 1 credit | # List new formations (/docs/api-reference/formations/listFormations) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Update a formation's metadata (/docs/api-reference/formations/updateFormation) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Add a company or person to a list (/docs/api-reference/lists/addListItem) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create a list (/docs/api-reference/lists/createList) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete a list (/docs/api-reference/lists/deleteList) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve a list (/docs/api-reference/lists/getList) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Overview (/docs/api-reference/lists) Your own lists of companies and persons. Free: managing your own data is not a data purchase. | Endpoint | Route | Cost | | -------------------------------------------------------------------------- | ----------------------------------------- | ---- | | [List your lists](/docs/api-reference/lists/listLists) | `GET /lists` | free | | [Create a list](/docs/api-reference/lists/createList) | `POST /lists` | free | | [Retrieve a list](/docs/api-reference/lists/getList) | `GET /lists/{list_id}` | free | | [Update a list](/docs/api-reference/lists/updateList) | `PATCH /lists/{list_id}` | free | | [Delete a list](/docs/api-reference/lists/deleteList) | `DELETE /lists/{list_id}` | free | | [List the items of a list](/docs/api-reference/lists/listListItems) | `GET /lists/{list_id}/items` | free | | [Add a company or person to a list](/docs/api-reference/lists/addListItem) | `POST /lists/{list_id}/items` | free | | [Update a list item](/docs/api-reference/lists/updateListItem) | `PATCH /lists/{list_id}/items/{item_id}` | free | | [Remove an item from a list](/docs/api-reference/lists/removeListItem) | `DELETE /lists/{list_id}/items/{item_id}` | free | # List the items of a list (/docs/api-reference/lists/listListItems) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List your lists (/docs/api-reference/lists/listLists) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Remove an item from a list (/docs/api-reference/lists/removeListItem) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Update a list (/docs/api-reference/lists/updateList) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Update a list item (/docs/api-reference/lists/updateListItem) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create a monitor (/docs/api-reference/monitors/createMonitor) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete a monitor (/docs/api-reference/monitors/deleteMonitor) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve a monitor (/docs/api-reference/monitors/getMonitor) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Overview (/docs/api-reference/monitors) Watching a company for the event types you choose. Managing monitors is free; only starting one costs credits. | Endpoint | Route | Cost | | -------------------------------------------------------------- | ------------------------------- | ---------- | | [List monitors](/docs/api-reference/monitors/listMonitors) | `GET /monitors` | free | | [Create a monitor](/docs/api-reference/monitors/createMonitor) | `POST /monitors` | 10 credits | | [Retrieve a monitor](/docs/api-reference/monitors/getMonitor) | `GET /monitors/{monitor_id}` | free | | [Update a monitor](/docs/api-reference/monitors/updateMonitor) | `PATCH /monitors/{monitor_id}` | free | | [Delete a monitor](/docs/api-reference/monitors/deleteMonitor) | `DELETE /monitors/{monitor_id}` | free | # List monitors (/docs/api-reference/monitors/listMonitors) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Update a monitor (/docs/api-reference/monitors/updateMonitor) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve a person (/docs/api-reference/persons/getPerson) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Overview (/docs/api-reference/persons) Natural persons named in the register, the roles they hold and the shareholdings they hold, across every company. | Endpoint | Route | Cost | | ------------------------------------------------------------------------------------------------ | ----------------------------------- | ---------- | | [Retrieve a person](/docs/api-reference/persons/getPerson) | `GET /persons/{person_id}` | 10 credits | | [List the roles a person holds](/docs/api-reference/persons/listPersonRoles) | `GET /persons/{person_id}/roles` | 10 credits | | [List the companies a person holds interests in](/docs/api-reference/persons/listPersonHoldings) | `GET /persons/{person_id}/holdings` | 10 credits | # List the companies a person holds interests in (/docs/api-reference/persons/listPersonHoldings) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List the roles a person holds (/docs/api-reference/persons/listPersonRoles) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Check service health (/docs/api-reference/platform/getHealth) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Overview (/docs/api-reference/platform) Service health. The only endpoint that needs no API key and costs nothing. | Endpoint | Route | Cost | | -------------------------------------------------------------- | ------------- | ---- | | [Check service health](/docs/api-reference/platform/getHealth) | `GET /health` | free | # Overview (/docs/api-reference/reference-data) The codes other responses use — legal forms, courts, role types, industries, event types and event groups — with their labels. Free. | Endpoint | Route | Cost | | ----------------------------------------------------------------------- | ------------------- | ---- | | [List legal forms](/docs/api-reference/reference-data/listLegalForms) | `GET /legal-forms` | free | | [List register courts](/docs/api-reference/reference-data/listCourts) | `GET /courts` | free | | [List role types](/docs/api-reference/reference-data/listRoleTypes) | `GET /role-types` | free | | [List event types](/docs/api-reference/reference-data/listEventTypes) | `GET /event-types` | free | | [List event groups](/docs/api-reference/reference-data/listEventGroups) | `GET /event-groups` | free | | [List industries](/docs/api-reference/reference-data/listIndustries) | `GET /industries` | free | # List register courts (/docs/api-reference/reference-data/listCourts) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List event groups (/docs/api-reference/reference-data/listEventGroups) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List event types (/docs/api-reference/reference-data/listEventTypes) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List industries (/docs/api-reference/reference-data/listIndustries) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List legal forms (/docs/api-reference/reference-data/listLegalForms) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List role types (/docs/api-reference/reference-data/listRoleTypes) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Filter companies by structured criteria (/docs/api-reference/search/filterCompanies) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Overview (/docs/api-reference/search) Turning a name, a register number or a set of filters into companies and persons. Start here: every other endpoint takes the `id` these return. | Endpoint | Route | Cost | | ----------------------------------------------------------------------------------------- | ------------------------------- | ---------- | | [Suggest companies and persons as you type](/docs/api-reference/search/listSuggestions) | `GET /search/suggestions` | 1 credit | | [Find companies by name or register identity](/docs/api-reference/search/searchCompanies) | `GET /search/companies` | 1 credit | | [Filter companies by structured criteria](/docs/api-reference/search/filterCompanies) | `POST /search/companies/filter` | 25 credits | | [Find persons by name](/docs/api-reference/search/searchPersons) | `GET /search/persons` | 1 credit | # Suggest companies and persons as you type (/docs/api-reference/search/listSuggestions) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Find companies by name or register identity (/docs/api-reference/search/searchCompanies) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Find persons by name (/docs/api-reference/search/searchPersons) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}