German Handelsregister API — Company Data & KYB
Pricing
from $0.005 / actor start
German Handelsregister API — Company Data & KYB
German company data & KYB API. Look up firms across all 5 German registers via handelsregister.de: Handelsregister (HRB/HRA), Genossenschaft (eG), Gesellschaftsregister (eGbR), Partnerschaft & Verein. Returns JSON — registration number, court, legal form, status, address. Firmendaten. Unofficial.
Pricing
from $0.005 / actor start
Rating
0.0
(0)
Developer
Patrick Lierse
Maintained by CommunityActor stats
0
Bookmarked
10
Total users
2
Monthly active users
9 days ago
Last modified
Categories
Security
Limited permissionsShare
German Handelsregister Lookup — all five public registers
Look up German entities in the official register portal (handelsregister.de) and get clean, structured JSON back: registration number, court, legal form, status and more.
One portal, five registers — all covered:
| Register | Section | Typical entities |
|---|---|---|
| Handelsregister | HRB / HRA | GmbH, AG, SE, UG, KG, OHG, e.K. |
| Genossenschaftsregister | GnR | eG — Volksbanken, Raiffeisen, housing & energy co-ops |
| Gesellschaftsregister (new since 2024) | GsR | eGbR — registered civil-law partnerships (property-holding GbRs) |
| Partnerschaftsregister | PR | PartG / PartG mbB — law, tax & audit firms |
| Vereinsregister | VR | e.V. — registered associations |
The same search handles every section — pass a name, or a register number with
its prefix (HRB 50000, GnR 23, GsR 4711, PR 512, VR 642). The
Gesellschaftsregister (eGbR) is a 2024 register that no other Apify actor
covers — useful for verifying the registered GbRs that now appear in land and
beneficial-ownership records.
Unofficial tool. This actor is not affiliated with or endorsed by the Bundesministerium der Justiz or any German authority. It retrieves the same publicly available register data any person can view free of charge on the official portal — register publicity is statutory (§ 9 HGB for the commercial register; equivalent provisions govern the cooperative, partnership, association and company registers, all free and login-free via the common portal since DiRUG, Aug 2022). It does so politely: hard rate limit, caching, no personal data by default.
Example
Input
{"companyName": "Allianz SE","maxResults": 1}
Output (one dataset item per company match, trimmed)
{"companyName": "Allianz SE","registrationNumber": "HRB 164232","registrationCourt": "München","legalForm": "SE","registeredAddress": "München","status": "aktuell"}
(Full records also include registrationDate, persons, sourceUrl,
cachedAt and dataSource — see the field notes below.)
Beyond the commercial register — the same actor resolves cooperatives, partnerships, associations and the new eGbR register. Real results from a live QA run (2026-06-13):
{ "companyName": "Berliner Volksbank eG", "registrationNumber": "GnR 23", "registrationCourt": "Berlin (Charlottenburg)", "legalForm": "eG", "status": "aktuell" }{ "companyName": "eGbR Lützowgärten KH", "registrationNumber": "GsR 4711", "registrationCourt": "Berlin (Charlottenburg)", "legalForm": "eGbR", "status": "aktuell" }{ "companyName": "Noerr Partnerschaftsgesellschaft mbB …", "registrationNumber": "PR 512", "registrationCourt": "München", "legalForm": "PartG mbB","status": "aktuell" }
Associations (VR) carry no legal-form suffix, so legalForm is null for
them by design — the register type is always available from the
registrationNumber prefix.
What it does
You give it a company name and/or a registration number (e.g. HRB 50000),
optionally a registration court (Amtsgericht). It searches the official
register portal and returns up to maxResults structured records. Successful
lookups are cached for 72 hours — registry data changes slowly, and cache
hits cost zero requests against the portal.
Rate limits — read this before bulk use
The portal's terms of use cap automated retrieval at 60 queries per hour. This actor enforces that limit internally with a sliding-window limiter (strictly at most 60 requests in any rolling hour, budget persisted across runs) — it is a feature, not a bug, and it cannot be disabled:
- When the budget is exhausted, the actor waits (it does not fail, does not rotate proxies, does not evade). A run that needs more requests than the hourly budget simply takes longer.
- The internal queue accepts at most 200 pending lookups. Beyond that the run fails fast with a clear error rather than accepting work it cannot politely complete.
- Repeat lookups within 72 hours are served from cache instantly and consume no quota.
If you need thousands of fresh records per hour, this is the wrong tool — and under the portal's terms, so is every other tool.
Input
| Field | Type | Default | Notes |
|---|---|---|---|
companyName | string | — | Name or keywords. Required if no registrationNumber. |
registrationNumber | string | — | Ideally with section prefix: HRB/HRA, GnR, GsR, PR, VR (e.g. GnR 23, HRB 158855 B). Spacing/case don't matter (hrb50000 works). A bare number (50000) is looked up as HRB, then HRA — see assumedRegisterType in SEARCH_STATUS. Required if no companyName. |
registrationCourt | string | — | Amtsgericht name, e.g. München, Berlin (Charlottenburg), AG Hamburg. Acts as a filter: if no matching entry is registered at that court, the run returns zero rows with status court_mismatch instead of an entry from another court. If the portal truncated the hit set before reaching that court, the status is court_not_checked_truncated instead (a match may exist). |
maxResults | integer | 10 | 1–100. |
Example input
{"companyName": "Allianz SE","maxResults": 2}
Example output (real result from a live QA run, 2026-06-11)
{"companyName": "AREVA GmbH","registrationNumber": "HRB 7817","registrationCourt": "Fürth","federalState": "Bayern","legalForm": "GmbH","registeredAddress": "Erlangen","registrationDate": null,"status": "aktuell","formerNames": ["Siemens Nuclear Power GmbH", "Framatome ANP GmbH", "AREVA NP GmbH"],"formerSeats": ["Erlangen"],"persons": [],"sourceUrl": "https://www.handelsregister.de/rp_web/erweitertesuche/welcome.xhtml","cachedAt": "2026-06-11T21:28:00.000Z","dataSource": "handelsregister.de/erweitertesuche"}
The search result grid reliably provides name, court, register number, federal
state (Bundesland), seat, status, and company history — formerNames and
formerSeats (the register's renaming/relocation trail; null when none).
These are all company data (no personal-data dimension).
Field availability honesty: registrationDate and persons — plus deeper
fields some competitors show (business purpose/Gegenstand, capital,
representatives/Prokuristen) — live only in the full filing document (a deep
record, the SI/AD/CD extracts). The portal serves those only through
JavaScript command-link postbacks that this plain-HTTP actor deliberately does
not replicate (no headless browser, no anti-bot workarounds). This actor does
not retrieve filing documents or named persons: persons is always empty
and registrationDate is always null. We do not fabricate these fields.
Searching by name: know these two limitations
1. The register matches registered legal names, not brands.
"Siemens Energy AG" and "BASF SE" resolve fine; "BMW AG" does not — the
registered name is "Bayerische Motoren Werke Aktiengesellschaft", which
contains neither "BMW" nor "AG". For brand-only names, search by
registrationNumber (e.g. HRB 42243) and add registrationCourt
(e.g. München) to disambiguate — register numbers repeat across courts and
the court filter is applied locally by the actor. Results are relevance-ranked
before maxResults is applied, because the portal itself returns up to 100
alphabetically sorted fuzzy matches.
2. The portal caps large hit sets at 100 — and hides that it did.
When more than ~100 entries match your keywords, handelsregister.de returns an
arbitrary 100 of them, sorts those alphabetically, and reports the truncated
count as if it were the total. The company you asked for can be missing
outright. This bites common single-word names: Allianz alone matches
hundreds of clubs and associations, and a search for "Allianz SE" comes back
without Allianz SE in it.
The actor recovers from this automatically. When it detects a capped hit
set that holds no exact match for your query, it re-issues the search once
using the portal's exact registered name mode, which is not subject to the
cap — "Allianz SE" then returns HRB 164232, München, as it should. Returned
records carry narrowedSearch: true when this happened. It costs one extra
search and only fires when the cap actually bit.
That retry is literal, so it finds nothing when the registered name differs from what you typed — a partial name ("Volkswagen"), or a legal form the register spells out ("Volkswagen AG" does not match the registered "VOLKSWAGEN AKTIENGESELLSCHAFT"). When it comes back empty, the actor falls back to reporting the truncation honestly rather than guessing:
- every returned record carries
resultsTruncated: truewhen the hit set was capped; - the run's key-value store holds
SEARCH_STATUSwithreportedTotal,rowsReturnedByPortal,exactNameMatch,narrowedSearchand astatusofok,truncatedorlow_confidence_suppressed; - and if the set was capped, no result carries the exact name you asked
for, you searched by name (not register number) with a legal form in the
query, and
maxResults <= 5, the actor returns zero results rather than a confident-looking wrong company. The candidates it declined to return are logged and listed inSEARCH_STATUS.suppressedCandidates.
For a KYB or onboarding workflow a wrong register number is worse than no
answer, which is why the default is to abstain. To get the ranked candidates
anyway, raise maxResults above 5; to pin an exact entity, use
registrationNumber + registrationCourt.
Pricing (pay per event)
| Event | Price | When charged |
|---|---|---|
| Actor start | $0.005 | once per run |
| Structured result | $0.004 | per record returned ($4 per 1,000) |
There are no other charges. Examples: 1,000 basic company lookups ≈ $4.01–$4.05 depending on how many runs you batch them into.
Run outcome — how to tell "not found" from "error"
Every run writes SEARCH_STATUS to its key-value store and sets the run's
status message. Branch on SEARCH_STATUS.outcome:
outcome | status | Run | Meaning |
|---|---|---|---|
found | ok / truncated | SUCCEEDED | Rows in the dataset. truncated: the portal capped the hit set (see resultsTruncated). |
not_found | not_found | SUCCEEDED | The register answered: no entry matches. A real answer. |
abstained | low_confidence_suppressed / court_mismatch / court_not_checked_truncated | SUCCEEDED | Candidates exist, but none is safe to return as the company. Candidates are listed in suppressedCandidates (not in the dataset). court_mismatch: the full hit set was checked and nothing is registered at your court. court_not_checked_truncated: the portal cut the hit set at its row cap before any entry from your court, so a match may exist — retry with a more specific name or the register number. |
invalid_input | invalid_input | SUCCEEDED | The query can't be run: blank input (reason: missing_query) or rejected by the portal (reason: rejected_by_portal, portal message in detail). |
error | error | FAILED | No answer — source down, network, or parser. errorType: source_unavailable, source_rejected, network_error, parse_error, robots_disallowed, internal_error. Safe to retry later. |
An error is never reported as "not found", and "not found" never fails the run.
GDPR note
Company register data is public by statutory design (§ 9 HGB). This actor
does not retrieve the names of directors or officers. See COMPLIANCE.md in the repository for the
operator's Legitimate Interest Assessment.
Every HTTP request the actor makes is timestamped and saved to the run's
key-value store under AUDIT_LOG.
Telemetry disclosure: this Actor logs search queries and error patterns to improve reliability, and stores no personal data. Concretely: run outcome, error type, duration, result counts and the shape of the input (which fields were set, number format). Query text is kept only when it is a company name with a corporate legal form (GmbH, AG, SE, KG, …); names of sole traders (e.K.) or anything without a corporate form are never stored. No user, account or run identifiers are recorded.
Troubleshooting
- "results grid not found" — the portal changed its markup (it has before).
The raw HTML is saved to the run's KV store under
PARSE_FAILURE_HTML; please report it. No fabricated data is ever emitted. - Slow run — that's the 60/hour limit working as designed; see above.
- The portal itself warns that its search can be flaky. Precise queries (exact name, or register number + court) work best.
Contact
Support: patrick.lierse@googlemail.com
Changelog
0.1.14 — 2026-10-28
- New status
court_not_checked_truncated(outcomeabstained). When you giveregistrationCourtand the portal truncated a large hit set before any entry from that court, the run used to reportcourt_mismatch, which wrongly implied nothing is registered there (e.g.Stadtwerke+Passaumissed Stadtwerke Passau GmbH).court_mismatchnow means the full hit set was checked. includePersonsremoved together with the unused "deep record" price event. It never returned data and was never charged. Old inputs that still send it run normally; the option is ignored with a log line.
0.1.10 — 2026-10-25 (output changes — please read if you automate this Actor)
- New field
matchConfidenceon every row:high(register number or exact registered name),medium(name starts with / contains the whole query),low(related name only). - Court is now a filter (
registrationCourt): no entry at that court → zero rows, statuscourt_mismatch. Previously an entry from a different court could be returned (e.g.HRB 50000+Münchenreturned Deutsche Bahn, Berlin). - Tie-break ordering: when two entries match a name equally well, companies (HRB, HRA) now rank before cooperatives, partnerships and associations (VR). Previously alphabetical order decided (
BASFreturned "BASF - Siedlergemeinschaft", VR, instead of BASF SE). - Register numbers normalised:
hrb50000,HRB 050000, Berlin suffixes (HRB 158855 B) and bare numbers (50000→ HRB, then HRA) now work. Bare numbers used to fail the run. - New outcome model (see Run outcome above): blank input and portal-rejected queries now finish with
invalid_inputinstead of failing; "no match" reportsnot_found(wasokwith zero rows); genuine errors still fail the run, with anerrorType. - Parallel runs in one account now share the 60 requests/hour budget correctly (previously they could exceed it). Parallel runs may therefore wait longer.
- Cached results from earlier versions are discarded once, so no pre-fix answer is served.
- Reliability telemetry added (see GDPR note).
0.1.9 — 2026-09-06 — truncation detection, exact-name retry, confidence floor.