Skip to main content

firmendata-python

Official Python client for the firmendata API — data on 2.4 million German companies from the Unternehmensregister and Handelsregister: register search, parsed annual financial statements, company profiles, register documents, and ownership chains for KYC.

PyPI Python License: MIT

pip install firmendata

Try it without signing up

Company-name autocomplete is free and needs no API key:

from firmendata import FirmenData

for hit in FirmenData().autocomplete("siemens")["data"]:
    print(hit["eu_id"], hit["display_name"])

Keyless calls are rate limited, modestly and by address — enough to try the API, back a search box, or run low-volume queries. Add a key for substantially higher limits plus every other endpoint. On a 429, honour Retry-After; the client already does this for you.

With an API key

Create one at firmendata.com — the free plan includes 100 credits.

from firmendata import FirmenData

fd = FirmenData(api_key="firmendata_live_...")

Search the register

The main entry point: 37 filters over all 2.4 million companies. Different filters combine with AND, repeated values with OR.

results = fd.search(
    bundesland=["Bayern", "Baden-Württemberg"],
    industry_slug=["manufacturing"],
    total_assets_min=1_000_000,
    legal_status=["active"],
    sort="total_assets",
    limit=25,
)

for hit in results["data"]:
    print(hit["display_name"], hit["address"]["city"], hit["total_assets"])

if results["pagination"]["has_more"]:
    next_page = fd.search(cursor=results["pagination"]["next_cursor"])

Values are case-insensitive and tolerate German spelling both ways — gmbh, muenchen, NRW and Bavaria all resolve. Filter by legal form, legal status, register court, federal state, city, industry, founding date, size, web presence, connected person or EU public-procurement role; see the filter reference.

Filtering on size? Use total_assets, not revenue. Small and medium-sized German companies file abridged accounts — a balance sheet, but no profit-and-loss statement and no headcount. A revenue or employee bound therefore narrows your results to the minority that publish a full P&L, while the balance-sheet total is available for every filing company.

Company profile

eu_id = "DEB1103R_HRB123456"  # from search or autocomplete

company = fd.get_company(eu_id)
history = fd.get_history(eu_id)   # chronological register entries

Identity and seat, register reference, legal status resolved from the merged Handelsregister and Insolvenzbekanntmachungen timelines, industry classification, contact details and web presence.

Financial statements

Filed annual accounts, parsed into figures rather than handed to you as PDFs. The deepest part of the dataset: German companies must publish, and we parse what they file into structured multi-year figures.

financials = fd.get_financials(eu_id)

summary = financials["summary"]
if summary:
    print(summary["latest_fiscal_year"], summary["latest_total_assets"])

for year in financials["history"]["metrics"]:
    print(year["year"], year["balance_sheet_total"], year["revenue"], year["profit"])

history also carries the structured profit_and_loss, assets and liabilities_and_equity rows as filed, plus employee_history and the underlying financial_publications.

summary is None when nothing is on file. Within it, figures resolve to the most recent filing that actually carries each one, so revenue and profit can come from different fiscal years — don't assume two share a year when computing a ratio.

Documents

doc = fd.download_document(eu_id, file_type="CD")

Aktueller and Chronologischer Abdruck, Gesellschafterliste, Satzung, Anmeldung and Musterprotokoll, as presigned download URLs.

Ownership: shareholders and UBO

Cap tables and beneficial-owner chains, for KYC and AML workflows.

Pass fetch_realtime=True on both of these. Unlike the endpoints above, which read an index we keep continuously fresh, cap tables are parsed from the filed Gesellschafterliste on demand — the flag fetches and parses the current filing for the company (and, for get_ubo, every German company in its ownership chain). Without it you are limited to whatever has already been parsed, and will often get not_filed for a company that has in fact filed. It costs more credits and takes a few seconds per company in the chain, which is the right trade for a KYC check.

cap = fd.get_shareholders(eu_id, fetch_realtime=True)

if cap["coverage"]["status"] == "available":
    for s in cap["as_of_snapshot"]["shareholders"]:
        print(s["display_name"], s["share_percent"])

ubo = fd.get_ubo(eu_id, fetch_realtime=True)
print(ubo["coverage"]["status"], ubo["beneficial_owners"])

What limits these:

  • Only GmbH, UG and gGmbH file a Gesellschafterliste. For an AG, KG, e.K. or any other form there is no cap table to read and coverage["status"] is not_applicable — not an error, and not something a retry will fix.
  • Without fetch_realtime, not_filed does not mean "never filed." It means no parsed cap table is on hand for that company yet. Re-request with the flag before concluding anything about a company's ownership.
  • Always branch on coverage["status"], never on an empty list. The two endpoints have different vocabularies: get_shareholdersavailable | not_filed | not_applicable | token_limit_reached (the filing was too large to parse); get_uboavailable | partial | not_filed | not_applicable. partial is the one to handle: an unresolved branch could still hide a beneficial owner, so read potential_beneficial_owners and coverage["reason"] rather than treating the result as complete.
  • An empty beneficial_owners is a real answer, not a failure: it means nobody crosses the 25% threshold. Fictional UBO under §3 Abs. 2 S. 5 GwG is not surfaced.
  • Attribution is all-or-nothing, not multiplicative. A holds 60% of H and H holds 30% of the root → A is a UBO at 30%, not 18%. Each link is independently tested against the 25% threshold, so a sub-threshold link breaks the chain entirely.

Subscriptions

Get notified when a company's data changes, instead of polling:

sub = fd.create_subscription(
    eu_id=eu_id,
    subscription_type="shareholders",  # or details, history, ubo, doc_*
    cadence="weekly",                  # immediately | daily | weekly | monthly
    notification_type="webhook",
    webhook_url="https://example.com/hooks/firmendata",
)

Webhook bodies are HMAC-SHA256 signed — verify X-Firmendata-Signature against the secret returned at creation. Omit notification_type to poll list_events() on your own schedule instead.

Async

Same methods, same semantics — every method above has an awaitable twin:

import asyncio
from firmendata import AsyncFirmenData

async def main():
    async with AsyncFirmenData(api_key="firmendata_live_...") as fd:
        company = await fd.get_company("DEB1103R_HRB123456")
        print(company["display_name"])

asyncio.run(main())

Errors

Every failure is a typed exception carrying the API's RFC 7807 problem detail, including a request_id you can quote to support.

from firmendata import FirmenData, InsufficientCreditsError, RateLimitError

try:
    fd.get_ubo(eu_id)
except InsufficientCreditsError:
    ...                      # top up or upgrade
except RateLimitError as e:
    ...                      # e.retry_after is the server's own hint
Exception Status Meaning
AuthenticationError 401 Missing/invalid key, or a keyless call used a paid feature
TokenExpiredError 401 Key expired
InsufficientCreditsError 402 Balance too low for this call
NotFoundError 404 No such company, subscription or event
ConflictError 409 Conflicts with existing state
ValidationError 422 Bad parameters — see .errors for the fields
RateLimitError 429 Retry budget exhausted — see .retry_after
ServerError 5xx Retried automatically for idempotent calls
APIConnectionError / APITimeoutError No response at all

Retries

Automatic and deliberately conservative:

  • 429 is always retried, on any method — the server rejects rate-limited calls before the handler runs, so nothing happened and nothing was billed. The server's Retry-After is used verbatim.
  • 5xx and connection failures are retried only for idempotent methods. A create_subscription that times out may already have been applied; replaying it would create a second one.
  • Backoff is exponential with full jitter, so clients that trip the same limit together don't all return at the same instant.

Tune with FirmenData(max_retries=...); 0 disables it.

Types

Responses are plain dictionaries described by generated TypedDicts, so editors complete every field and mypy checks them — with no pydantic dependency to collide with your own. The only runtime requirement is httpx.

Both src/firmendata/types.py and src/firmendata/params.py are generated from contracts/openapi.v1.json, a vendored copy of the published spec:

python scripts/generate_types.py

CI regenerates them and fails if the result differs from what is committed, so the SDK cannot silently drift from the API it targets.

Development

pip install -e '.[dev]'
pytest              # no network, no credentials
mypy && ruff check

Links

License

MIT — see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

firmendata-0.2.0.tar.gz (85.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

firmendata-0.2.0-py3-none-any.whl (28.1 kB view details)

Uploaded Python 3

File details

Details for the file firmendata-0.2.0.tar.gz.

File metadata

  • Download URL: firmendata-0.2.0.tar.gz
  • Upload date:
  • Size: 85.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for firmendata-0.2.0.tar.gz
Algorithm Hash digest
SHA256 e7f2948cf69441442c4a248d6e4b405fbeed7187870fcdc94a47f980d7d5f112
MD5 3bb61e7f92bca8da596a2746b55da9cc
BLAKE2b-256 385816c4a2142996fc087d4469131d9a6bbbec86df905aebc6596e23e9976a15

See more details on using hashes here.

Provenance

The following attestation bundles were made for firmendata-0.2.0.tar.gz:

Publisher: publish.yml on FirmenData/firmendata-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file firmendata-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: firmendata-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 28.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for firmendata-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c7014c59590353f1c1fbcffea50a76b5da9602dbc3db18bfb88a9b522e057cff
MD5 06f08d170c2eb11428d5275f50db10f9
BLAKE2b-256 73903b810f20029d80e46e8fe123e4e76052dc79a1c4730974f7d58b1a67585f

See more details on using hashes here.

Provenance

The following attestation bundles were made for firmendata-0.2.0-py3-none-any.whl:

Publisher: publish.yml on FirmenData/firmendata-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.1

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page