Filtering
Building a segment with a structured query rather than a search term.
POST /search/companies/filter answers a
structured question — "GmbHs in Bavaria, founded since 2025, with a managing director over
60" — rather than a text query. It costs 1 credit per 10 returned results, nothing when none is found, and a page
holds up to 1,000 companies.
Shape
{
"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
{
"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
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.
{
"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.
namematches the name of a company that holds a role, or a person's first, last or full name ("Hans Müller");first_nameandlast_namematch one of a person's names alone. All three match a substring (contains).role_typecan only be["authorized_signatory"](Prokuristen) for now: the register data records no role type for the other roles, so any other value is answered400.ageis 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 (gteandlte, one or both, inclusive). An age on its own answers400: 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'sstatusdoes not count: it leaves almost every row in the scan. A shareholder'sholder_typedoes not count either: it only picks person or company holders. A shareholder criterion, such as aperson_id, does count for a role's age. The same rule covers a Kommanditist'sageorbirth_date(type: ["limited_partner"]only picks that table). A fraction, a value outside 0–120, orgtegreater thanltealso answers400. Plain shareholders have no date of birth, soagethere answers400.
{
"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.
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
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,liquidationandterminatedare selected on the status each company's record publishes, alone or together, so anactivesegment never contains a company in liquidation andtotal_countis the size of the segment.unknownis 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
limit and cursor in the body page the result set: send each page's next_cursor back as
cursor — see
Pagination. Each call costs 1 credit per 10 results it returns; your balance
must hold the price of the limit you ask for before the call.