German Handelsregister API — Company Data & KYB avatar

German Handelsregister API — Company Data & KYB

Pricing

from $0.005 / actor start

Go to Apify Store
German Handelsregister API — Company Data & KYB

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

Patrick Lierse

Maintained by Community

Actor stats

0

Bookmarked

10

Total users

2

Monthly active users

9 days ago

Last modified

Security

Limited permissions

Share

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:

RegisterSectionTypical entities
HandelsregisterHRB / HRAGmbH, AG, SE, UG, KG, OHG, e.K.
GenossenschaftsregisterGnReG — Volksbanken, Raiffeisen, housing & energy co-ops
Gesellschaftsregister (new since 2024)GsReGbR — registered civil-law partnerships (property-holding GbRs)
PartnerschaftsregisterPRPartG / PartG mbB — law, tax & audit firms
VereinsregisterVRe.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

FieldTypeDefaultNotes
companyNamestring—Name or keywords. Required if no registrationNumber.
registrationNumberstring—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.
registrationCourtstring—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).
maxResultsinteger101–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: true when the hit set was capped;
  • the run's key-value store holds SEARCH_STATUS with reportedTotal, rowsReturnedByPortal, exactNameMatch, narrowedSearch and a status of ok, truncated or low_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 in SEARCH_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)

EventPriceWhen charged
Actor start$0.005once per run
Structured result$0.004per 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:

outcomestatusRunMeaning
foundok / truncatedSUCCEEDEDRows in the dataset. truncated: the portal capped the hit set (see resultsTruncated).
not_foundnot_foundSUCCEEDEDThe register answered: no entry matches. A real answer.
abstainedlow_confidence_suppressed / court_mismatch / court_not_checked_truncatedSUCCEEDEDCandidates 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_inputinvalid_inputSUCCEEDEDThe query can't be run: blank input (reason: missing_query) or rejected by the portal (reason: rejected_by_portal, portal message in detail).
errorerrorFAILEDNo 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 (outcome abstained). When you give registrationCourt and the portal truncated a large hit set before any entry from that court, the run used to report court_mismatch, which wrongly implied nothing is registered there (e.g. Stadtwerke + Passau missed Stadtwerke Passau GmbH). court_mismatch now means the full hit set was checked.
  • includePersons removed 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 matchConfidence on 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, status court_mismatch. Previously an entry from a different court could be returned (e.g. HRB 50000 + München returned 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 (BASF returned "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_input instead of failing; "no match" reports not_found (was ok with zero rows); genuine errors still fail the run, with an errorType.
  • 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.