Metadata-Version: 2.4
Name: handelsregister
Version: 0.8.0
Summary: Python SDK für den Zugriff auf die Handelsregister AI API
Author-email: Handelsregister Team <team@handelsregister.ai>
License: AGPL-3.0-only
Project-URL: Homepage, https://github.com/Handelsregister-AI/handelsregister
Project-URL: Bug Reports, https://github.com/Handelsregister-AI/handelsregister/issues
Keywords: handelsregister,api,german company data,business data
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.23.0
Requires-Dist: tqdm>=4.0.0
Requires-Dist: pandas>=1.0.0
Requires-Dist: openpyxl>=3.0.0
Requires-Dist: rich>=13.0.0
Dynamic: license-file

# Handelsregister Python SDK

[![PyPI version](https://img.shields.io/pypi/v/handelsregister.svg)](https://pypi.org/project/handelsregister/)
[![Python Versions](https://img.shields.io/pypi/pyversions/handelsregister.svg)](https://pypi.org/project/handelsregister/)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)

A modern Python client for the [Handelsregister.ai](https://handelsregister.ai) API. Structured, reliable, and fast access to the German commercial register (Handelsregister): company master data, financials, management, shareholders, UBOs, person profiles, and official PDF documents.

## Features

- **Company lookup** — `fetch-organization` with configurable feature flags
- **Person profiles** — `fetch-person` (Handelsregister roles + web data)
- **Search** — query or filters-only search with geo, registry, size, and financial filters
- **Signals** — cursor-paginated company changes with topic, company, and date filters
- **Monitoring & webhooks** — per-company monitors with signed webhook push, endpoint lifecycle, and receiver-side signature verification
- **Account** — profile, credits, usage, subscription, and API-key management
- **Financial data** — KPIs, balance sheet, P&L, full annual reports (MD/HTML)
- **Management** — current and past related persons with roles
- **Shareholders, UBOs, shareholdings** — who owns the company, who the company owns
- **Mergers & acquisitions** — transactions, succession, enterprise agreements, and control relationships
- **Representation schemes** — current and historical company- and person-role representation rules
- **News, publications, insolvency publications**
- **Website content** — structured Markdown, optimized for LLMs
- **Document downloads** — Gesellschafterliste, Gesellschaftsvertrag, AD/CD PDFs, and SI XML
- **Auth** — `x-api-key` header or Bearer token, plus token management
- **Batch enrichment** — resilient CSV/JSON/XLSX enrichment with snapshots
- **Live mode** — opt-in realtime lookups against the Handelsregister

## Installation

```bash
pip install handelsregister
```

## Authentication

You can authenticate in two ways:

**API key (recommended for server-to-server):**

```bash
export HANDELSREGISTER_API_KEY=your_api_key_here
```

```python
from handelsregister import Handelsregister

client = Handelsregister(api_key="your_api_key_here")
```

**Bearer token (fine-grained control, expiration):**

```bash
export HANDELSREGISTER_BEARER_TOKEN=your_token_here
```

```python
client = Handelsregister(bearer_token="your_token_here")
```

When both are provided, the bearer token wins.

For gateways or proxies that require additional request headers, provide them
explicitly. Managed authentication and User-Agent headers cannot be
overridden:

```python
import os

from handelsregister import Handelsregister

client = Handelsregister(
    api_key="your_api_key_here",
    extra_headers={
        "X-Gateway-Client-Id": os.environ["GATEWAY_CLIENT_ID"],
        "X-Gateway-Client-Secret": os.environ["GATEWAY_CLIENT_SECRET"],
    },
)
```

Additional headers can also come from the environment:

```bash
export HANDELSREGISTER_EXTRA_HEADERS='{"X-Gateway-Client-Id": "...", "X-Gateway-Client-Secret": "..."}'
```

Explicit `extra_headers` always win over this variable.

## Quick Start

### Company lookup

```python
from handelsregister import Handelsregister

client = Handelsregister()

result = client.fetch_organization(
    q="KONUX GmbH München",
    features=["related_persons", "financial_kpi", "shareholders"],
    ai_search="on-default",          # optional: enable AI search
    # realtime_mode="handelsregister-default",  # +10 credits for live data
)

print(result["name"], result["registration"]["register_number"])
```

### Object-oriented `Company` interface

```python
from handelsregister import Company

company = Company(
    "OroraTech GmbH München",
    features=[
        "related_persons",
        "financial_kpi",
        "balance_sheet_accounts",
        "shareholders",
        "ubos",
        "shareholdings",
        "mergers_and_acquisitions",
        "network",
        "annual_financial_statements",
        "news",
    ],
)

print(company.name, company.is_active)
print(company.formatted_address)

# Management
for person in company.current_related_persons:
    print(person["name"], "-", person["role"]["en"]["long"])

# Shareholders (who owns the company)
for entry in company.shareholders.entries:
    print(entry.display_name, entry.percentage)

# Ultimate beneficial owners
for ubo in company.ubos.resolved:
    print(ubo.name, ubo.percentage)

# Outbound shareholdings (what the company owns)
for holding in company.shareholdings.current:
    print(holding.organization_name, holding.percentage)

# Company- and person-level representation rules
for rule in company.representation_scheme.active:
    print(rule)

for director in company.related_person_entries.current:
    print(director.display_name, director.role_representation_scheme.active)

# M&A transactions
for transaction in company.mergers_and_acquisitions.transactions:
    print(transaction.date, transaction.headline_text("en"))

# Relationship graph (Pro/Max)
print(company.network.depth, len(company.network.nodes))
for connection in company.network.connections:
    print(connection.source.name, connection.label, connection.target.name)

# News
for article in company.news:
    print(article["publication_date"], article["title"])
```

### Person profiles

The `/v1/fetch-person` endpoint merges Handelsregister records with public web data. `ai_search` is always on for this endpoint (the 15-credit base cost includes the AI enrichment). `organization_q` is required to disambiguate common names.

```python
from handelsregister import Person

person = Person(
    person_q="Max Mustermann",
    organization_q="Beispielwerk Analytics GmbH",
    features=["shareholdings"],  # +5 credits, only if data is returned
)

print(person.canonical_name, "-", person.home_city)
print(person.bio)

for role in person.handelsregister_roles:
    print(role["name"], role["label"], role.get("start_date"), role.get("end_date"))

for holding in person.shareholdings.current:
    print(holding.organization_name, holding.percentage, holding.as_of)
```

### Search

```python
from handelsregister import (
    FilterCondition,
    Handelsregister,
    OrganizationStatus,
    OwnershipFilters,
    RangeFilter,
    SearchFilters,
    SearchSort,
    SortOrder,
)

client = Handelsregister()

page = client.search_organizations(
    limit=10,
    skip=0,
    filters=SearchFilters(
        city="München",
        legal_form_code="GmbH",
        status=OrganizationStatus.ACTIVE,
        pl_revenue=RangeFilter(gte=1_000_000, lte=5_000_000),
        ownership_filters=OwnershipFilters(
            owner_managed=True,
            oldest_owner_birth_date=FilterCondition(lte="1960"),
        ),
    ),
    sort=SearchSort.REVENUE,
    order=SortOrder.DESC,
    match_context=True,
)

print(page["total"])
for item in page["results"]:
    print(item["name"], item["registration"]["register_number"])
```

The API returns at most **30 organizations per request**. Passing `limit=31`
or higher raises `ValueError` instead of silently returning a truncated page.
Use `skip` for manual pagination, or let the lazy iterator fetch successive
pages:

```python
organizations = list(
    client.iter_search_organizations(
        q="tech",
        page_size=30,
        max_results=100,
    )
)
```

For 100 available matches this makes four requests with page sizes
`30`, `30`, `30`, and `10`. Each page is a separate billable API request;
stopping iteration early prevents subsequent pages from being fetched.

`q` may contain 2–500 characters and may be omitted when at least one filter is
supplied. `filters` may also be an ordinary dictionary. Supported keys cover
registration dates, legal form, exact organization status, liability type,
WZ/NACE industries, location and radius, register data, company size, employee
and financial ranges, plus the Pro/Max `ownership_filters`,
`executive_filters`, and `lifecycle_filters` groups.

Financial range dictionaries use `{"gte": minimum, "lte": maximum}` and are
sent under `financial_filters`. Advanced register-data conditions additionally
support `gt`, `lt`, `eq`, and `exists`; `FilterCondition` builds these objects.
Use `LocationCoordinates(lat=..., lon=...)` for a radius search. The SDK also
accepts the legacy `{"latitude": ..., "longitude": ...}` input and normalizes
it to the current API shape.

`sort` accepts the documented fields exposed by `SearchSort`, `order` accepts
`SortOrder.ASC` or `SortOrder.DESC`, and `match_context=True` requests the
matching ownership, executive, and lifecycle values under each result's
`_match_context` object. Advanced register-data filters require a Pro or Max
subscription; an insufficient plan raises `SubscriptionRequiredError`.

## Signals

Signals expose company changes through seven stable topic codes. Catalog
requests are free; successful list and detail requests cost 20 credits. Pages
contain 20 signals and use opaque cursor pagination.

### Signals endpoints

| HTTP endpoint | Python method | Successful request cost |
|---|---|---:|
| `GET /api/v1/signals` | `list_signals()` | 20 credits per page |
| `GET /api/v1/signals` | `iter_signals()` | 20 credits per fetched page |
| `GET /api/v1/signals/catalog` | `get_signal_catalog()` | Free |
| `GET /api/v1/signals/{signal_id}` | `get_signal()` | 20 credits |

`list_signals()` accepts these arguments:

| Python argument | Description |
|---|---|
| `cursor` | Opaque `pagination.next_cursor` value returned by the previous page |
| `topics` | One topic, a comma-separated string, or an iterable of `SignalTopic`/string values |
| `organization_ids` | One entity ID or an iterable of entity IDs |
| `from_date` | Inclusive publication-date lower bound as an ISO 8601 string, `date`, or `datetime` |
| `to_date` | Inclusive publication-date upper bound as an ISO 8601 string, `date`, or `datetime` |

`iter_signals()` accepts the same filters and adds `max_results`. It requests
the next page only when iteration reaches it.

```python
from handelsregister import Handelsregister, SignalTopic

client = Handelsregister()

page = client.list_signals(
    topics=[
        SignalTopic.CAPITAL_CHANGES,
        SignalTopic.TRANSFORMATIONS,
    ],
    organization_ids=[
        "0123456789abcdef0123456789abcdef",
        "fedcba9876543210fedcba9876543210",
    ],
    from_date="2026-07-01",
    to_date="2026-07-30",
)

for signal in page["signals"]:
    print(signal["event"]["id"], signal["event"]["topic"])

catalog = client.get_signal_catalog()
if page["signals"]:
    detail = client.get_signal(page["signals"][0]["event"]["id"])
    print(detail["signal"]["event"]["topic"])
```

For manual cursor navigation, send the cursor unchanged and preserve the
filters used for the first page:

```python
filters = {
    "topics": [SignalTopic.NEW_REGISTRATIONS],
    "from_date": "2026-07-01",
    "to_date": "2026-07-30",
}

first_page = client.list_signals(**filters)
next_cursor = first_page["pagination"].get("next_cursor")

if next_cursor:
    second_page = client.list_signals(cursor=next_cursor, **filters)
```

Multiple `organization_ids` use OR semantics: a returned Signal may belong to
any supplied entity ID. The SDK sends the IDs as one comma-separated query
value and preserves them while following cursors.

The lazy iterator handles this automatically. Every fetched page is a separate
billable request:

```python
for signal in client.iter_signals(
    topics=[SignalTopic.NEW_REGISTRATIONS],
    max_results=50,
):
    print(signal["organization"]["current_profile"]["name"])
```

### Signal topics

The public topics are available as both `SignalTopic` and `SIGNAL_TOPICS`.

| Code | Data covered | Plan requirement |
|---|---|---|
| `NEW_REGISTRATIONS` | Newly registered organizations | No additional topic gate |
| `MASTER_DATA_CHANGES` | Name, registered seat, address, or register changes | No additional topic gate |
| `CLOSURES` | Dissolution, liquidation, deletion, or expiration | No additional topic gate |
| `ROLE_HOLDER_CHANGES` | Management, board, and procuration changes | No additional topic gate |
| `CAPITAL_CHANGES` | Share, nominal, liable, or authorized capital changes | No additional topic gate |
| `INSOLVENCIES` | Openings, protective measures, and completed proceedings | Pro |
| `TRANSFORMATIONS` | Mergers, divisions, conversions, asset transfers, enterprise agreements, and squeeze-outs | Max |

Requests below the required plan return HTTP 403 with
`PLAN_REQUIRED` and cost zero credits. The SDK maps this response to
`SubscriptionRequiredError`.

### Signal response data

Each list entry contains:

- `event`: stable ID, topic, localized topic name, occurrence/publication dates,
  and date basis. `ROLE_HOLDER_CHANGES` events additionally carry a `type`
  (`ROLE_HOLDER_ENTRY` or `ROLE_HOLDER_EXIT`) with a localized `type_name`.
- `organization`: entity ID and the current organization profile.
- `parties`: topic-specific participants - for `ROLE_HOLDER_CHANGES` a
  `role_holder` with the person/organization entity plus its role `code` and
  `representation_scheme`.
- `register_entry`: entry number/date, phase, description, context, and flags.
- `source`: source kind.
- `details`: topic-specific structured data identified by `details.schema`.

Fields without a value are omitted rather than returned as `null`. A list
response also includes `pagination`, applied `filters`, `warnings`, and `meta`.
The pagination object reports the fixed limit, returned count, `has_more`, and
the next opaque cursor. There is no total count.

`get_signal()` returns the event inside a `signal` envelope:

```python
detail = client.get_signal("0123456789abcdef0123456789abcdef")
signal = detail["signal"]
request_cost = detail["meta"]["request_credit_cost"]
```

The catalog response contains the public topics and their descriptions,
whether data has been observed for each topic, `catalog_version`, and server
`capabilities`.

## Monitoring & Webhooks

Monitoring watches companies you select and pushes new normalized
commercial-register changes to your HTTPS endpoints through signed webhooks.
It shares the topic vocabulary with Signals but is an independent product;
each webhook links to the Signals detail API for on-demand deep data.

Reads are free. Monitor mutations work with an API key or a Bearer token
carrying `account:read` plus `monitoring:manage`. Endpoint creation, secret
rotation, enable/disable, and archive additionally require a Bearer token
with `account:read` plus `account:keys`.

### Setting up a receiver

```python
from handelsregister import Handelsregister

client = Handelsregister(bearer_token="YOUR_ADMIN_TOKEN")

# 1. Register the endpoint; store the one-time whsec_ secret immediately.
created = client.create_webhook_endpoint(
    name="Production receiver",
    url="https://hooks.example.com/handelsregister",
    headers={"x-tenant": "customer-42"},  # optional, write-only
)
endpoint_id = created["endpoint"]["id"]
signing_secret = created["signing_secret"]  # shown exactly once

# 2. Your receiver must echo data.challenge in a `webhook-verification`
#    header with any 2xx status; then trigger the challenge. A successful
#    first verification activates the endpoint immediately.
result = client.verify_webhook_endpoint(endpoint_id)  # {"verified": true/false}

# 3. Optionally send a signed test event. enable_webhook_endpoint() is only
#    needed to reactivate an endpoint after a disable.
client.test_webhook_endpoint(endpoint_id)
```

### Creating a monitor

```python
# Pricing is informational and useful for estimating the cycle cost.
pricing = client.get_monitoring_pricing(poll_interval_days=7)

created = client.create_monitor(
    entity_id="cc78cf0b230aeae35c6df7ba31989bb9",
    poll_interval_days=7,
    endpoint_ids=[endpoint_id],
    label="BMW AG",
)
monitor = created["monitor"]  # status: "initializing", baseline queued, 0 credits

detail = client.get_monitor(monitor["id"])
detail["billing_cycle"]                         # active cycle summary, or None
detail["recent_runs"]                           # newest 20 poll runs
client.update_monitor(monitor["id"], 14)          # prospective interval change
client.pause_monitor(monitor["id"])
client.resume_monitor(monitor["id"])
client.archive_monitor(monitor["id"])           # archive, never hard-delete
```

The free all-topic baseline runs asynchronously and suppresses historical
observations. It can complete within seconds, at which point activation
charges a 10-credit cycle floor covering five complete successful checks in
a rolling 30-day cycle; each further complete successful check costs
2 credits (`max(10, 2 * checks)`). Failed, partial, superseded, or unfunded
checks add no run charge, and pausing or archiving never refunds the floor —
archive while still `initializing` to stop a monitor before activation.
Every account receives the five core topics; `INSOLVENCIES` needs Pro or
Max and `TRANSFORMATIONS` needs Max. Inaccessible observations are
terminally suppressed, not replayed after an upgrade.

### Verifying deliveries in your receiver

Delivery is at-least-once with no ordering guarantee, so verify the exact
raw request bytes and deduplicate on the message id:

```python
from handelsregister.webhooks import construct_event, verification_response_headers

# e.g. in a Flask/FastAPI handler
event = construct_event(raw_body_bytes, request_headers, signing_secret)

if event["type"] == "endpoint.verification":
    return Response(status=204, headers=verification_response_headers(event))

if event["type"] == "organization.signal.detected":
    signal = event["data"]["signal"]
    link = event["data"]["links"]["signal"]  # Signals detail API (20 credits on success)
```

`construct_event()` checks the `v1,<base64>` HMAC-SHA256 signature over
`webhook-id.webhook-timestamp.raw_body`, accepts the current and predecessor
secret during the seven-day rotation grace (pass a list of secrets), and
enforces a configurable timestamp tolerance. Samples always set
`data.sample=true`; detected events never do. Respond with any 2xx quickly;
3xx/4xx/5xx and timeouts are retried for roughly three days across ten
attempts, HTTP 410 disables the endpoint immediately, and five consecutive
exhausted deliveries disable it too.

### Idempotency

Every mutation requires an `Idempotency-Key`. The SDK generates a compliant
key automatically and reuses it across its internal retries, so transient
errors never double-charge or double-create. Pass `idempotency_key=` to make
retries across process restarts safe as well: an exact retry within 24 hours
replays the original response — including the same resource id — with
`client.last_idempotency_status` set to `"replayed"` instead of
`"created"`. Reusing a key with different parameters raises
`IdempotencyConflictError`. Requests rejected by validation (HTTP 400/422)
never claim their key, so the same key can be retried after fixing the
request. Never re-drive an ambiguous 409 on a verify/test operation with a
fresh key, because the API cannot prove whether your receiver saw the
ambiguous request.

### Delivery history

```python
client.list_webhook_endpoints()                  # all non-archived endpoints (max 10)
client.list_webhook_deliveries(endpoint_id)      # newest 50 delivery summaries
client.retry_webhook_delivery("del_...")         # re-drive a retained failed delivery
client.list_webhook_events()                     # newest 50 event summaries
client.rotate_webhook_endpoint_secret(endpoint_id)  # new one-time whsec_ secret
client.disable_webhook_endpoint(endpoint_id)
client.archive_webhook_endpoint(endpoint_id)
```

Event payloads are retained encrypted for 30 days, delivery attempt audit
rows for 90 days.

## Account and Usage

Account endpoints are read-only except for API-key creation and revocation.
Every Account request costs zero credits.

### Account endpoints

| HTTP endpoint | Python method | Authentication |
|---|---|---|
| `GET /api/v1/account` | `get_account()` | API key or `account:read` Bearer token |
| `GET /api/v1/account/credits` | `get_account_credits()` | API key or `account:read` Bearer token |
| `GET /api/v1/account/usage` | `get_account_usage()` | API key or `account:read` Bearer token |
| `GET /api/v1/account/usage/transactions` | `get_account_usage_transactions()` | API key or `account:read` Bearer token |
| `GET /api/v1/account/subscription` | `get_account_subscription()` | API key or `account:read` Bearer token |
| `GET /api/v1/account/api-keys` | `list_api_keys()` | API key or `account:read` Bearer token |
| `POST /api/v1/account/api-keys` | `create_api_key()` | Bearer token with `account:keys` |
| `DELETE /api/v1/account/api-keys/{id}` | `revoke_api_key()` | Bearer token with `account:keys` |

```python
from handelsregister import Handelsregister

client = Handelsregister()

profile = client.get_account()
credits = client.get_account_credits()
subscription = client.get_account_subscription()
keys = client.list_api_keys()  # masked values only

usage = client.get_account_usage(
    from_date="2026-07-01",
    to_date="2026-07-30",
    group_by="day",
)

transactions = client.get_account_usage_transactions(
    endpoint="/api/v1/signals",
    per_page=25,
)
```

`get_account_usage()` and `get_account_usage_transactions()` accept:

| Python argument | Applies to | Description |
|---|---|---|
| `from_date` / `to_date` | Both | ISO 8601 string, `date`, or `datetime`; defaults to the current month; maximum range is 366 days |
| `group_by` | Usage | `"day"` or `"month"`; the server chooses a default based on range length |
| `endpoint` | Transactions | Exact endpoint filter, for example `/api/v1/signals` |
| `per_page` | Transactions | Page size from 1 to 100; default is 25 |
| `cursor` | Transactions | Opaque cursor returned in `pagination.next_cursor` |

A date-only `to_date` includes that entire day. Transactions can also be
consumed across all cursor pages:

```python
for transaction in client.iter_account_usage_transactions(per_page=100):
    print(transaction["endpoint"], transaction["credits"])
```

Account responses provide:

- Profile data: name, email, language, and current plan.
- Credits: remaining balance, bookings, and the next expiration date.
- Usage: selected period, request/credit totals, endpoint breakdown, and daily
  or monthly time-series buckets.
- Transactions: individual billed requests plus cursor pagination.
- Subscription: plan, status, billing period, and included features.
- API keys: active keys in masked form, creation time, and last usage time.

Creating or revoking an API key requires a Bearer token with the
`account:keys` ability. The full key is returned only by the creation response:

```python
import os

from handelsregister import Handelsregister

admin = Handelsregister(
    bearer_token=os.environ["HANDELSREGISTER_ADMIN_BEARER_TOKEN"],
)
created = admin.create_api_key()

try:
    new_key = created["api_key"]["key"]
finally:
    admin.revoke_api_key(created["api_key"]["id"])
```

### End-to-end Account and Signals demo

The repository includes a runnable script that pretty-prints the live
responses while redacting credentials:

```bash
# Account reads are free. Signals catalog + list + detail cost up to 40 credits.
python examples/account_signals_demo.py

# Account only (free)
python examples/account_signals_demo.py --account

# Signals only, without the 20-credit detail call
python examples/account_signals_demo.py --signals --skip-detail
```

For a temporary API-key create/verify/revoke round trip, set
`HANDELSREGISTER_ADMIN_BEARER_TOKEN` and run:

```bash
python examples/account_signals_demo.py \
  --account \
  --admin-key-roundtrip
```

## Document Downloads

```python
from handelsregister import Handelsregister, Company

client = Handelsregister()

# Get the company's entity_id
result = client.fetch_organization(q="KONUX GmbH München")
entity_id = result["entity_id"]

# Download documents directly from the client
client.fetch_document(
    company_id=entity_id,
    document_type="shareholders_list",       # Gesellschafterliste
    output_file="konux_shareholders.pdf",
)

client.fetch_document(
    company_id=entity_id,
    document_type="articles_of_association", # Gesellschaftsvertrag / Satzung
    output_file="konux_articles.pdf",
)

client.fetch_document(
    company_id=entity_id,
    document_type="AD",                      # Aktueller Ausdruck
    output_file="konux_current.pdf",
)

pdf_bytes = client.fetch_document(
    company_id=entity_id,
    document_type="CD",                      # Chronologischer Ausdruck
)

xml_bytes = client.fetch_document(
    company_id=entity_id,
    document_type="SI",                      # Structured information (XML)
    output_file="konux_structured.xml",
)

# Or via the Company helper
company = Company("OroraTech GmbH München")
company.fetch_document(
    document_type="shareholders_list",
    output_file="ororatech_shareholders.pdf",
)
```

### Available document types

| Document Type              | Description                                        |
|----------------------------|----------------------------------------------------|
| `shareholders_list`        | Gesellschafterliste                                |
| `articles_of_association`  | Gesellschaftsvertrag / Satzung / Statut            |
| `AD`                       | Aktuelle Daten (current excerpt)                   |
| `CD`                       | Chronologische Daten (historical excerpt)          |
| `SI`                       | Strukturierter Inhalt (XML)                        |

## Bearer Token Management

If you prefer managing bearer tokens over sharing an API key:

```python
client = Handelsregister(api_key="your_api_key_here")

# Create a new token. expires_at must lie in the future; omit it for a
# non-expiring token.
created = client.create_token(
    token_name="My Application",
    abilities=["account:read", "monitoring:manage"],
    expires_at="2027-01-01 00:00:00",
)
created["token"]      # the bearer token value - shown exactly once
created["abilities"]  # abilities actually granted

# List tokens; the create response has no id, so look it up here.
tokens = client.list_tokens()
token_id = next(
    t["id"] for t in tokens["tokens"] if t["name"] == "My Application"
)
client.revoke_token(token_id=token_id)

# Revokes every bearer token of the account - use deliberately.
client.revoke_all_tokens()
```

Ability notes: passing `["*"]` does not grant a wildcard - the server
replaces it with the defaults `api:data` and `account:read`. Request
additional abilities such as `monitoring:manage` explicitly. `account:keys`
cannot be self-granted through this endpoint; tokens for webhook-endpoint
administration must be created in the dashboard.

## Data Enrichment

Enrich a CSV/JSON/XLSX file of companies with Handelsregister data. Intermediate snapshots let you resume long-running jobs.

```python
from handelsregister import Handelsregister

client = Handelsregister()

client.enrich(
    file_path="companies.csv",
    input_type="csv",
    query_properties={
        "name": "company_name",   # map 'company_name' column to query
        "location": "city",       # map 'city' column to query
    },
    snapshot_dir="snapshots",
    params={
        "features": ["related_persons", "financial_kpi", "ubos"],
        "ai_search": "on-default",
    },
    output_file="companies_enriched.csv",
    output_type="csv",
)
```

Each output row keeps the input columns and adds the API response under
`_handelsregister_result` plus a `_in_file` marker.

There is also a DataFrame convenience:

```python
import pandas as pd
from handelsregister import Handelsregister

client = Handelsregister()
df = pd.read_csv("companies.csv")
enriched = client.enrich_dataframe(
    df,
    query_properties={"name": "company_name", "location": "city"},
    params={"features": ["financial_kpi"]},
)
```

## Command Line Interface

Installing the package exposes the `handelsregister` CLI. If the optional `rich` dependency is installed, commands render colorful tables.

```bash
# Company lookup (defaults: all standard features + AI search)
$ handelsregister fetch "KONUX GmbH München"

# Raw JSON
$ handelsregister fetch json "KONUX GmbH München"

# Opt-in to realtime mode for live register data
$ handelsregister fetch "KONUX GmbH München" --realtime-mode handelsregister-default

# Person profile
$ handelsregister person \
    --person "Max Mustermann" \
    --organization "Beispielwerk Analytics GmbH" \
    --feature shareholdings

# Search (maximum 30 results per request)
$ handelsregister search "tech" --postal-code 80992 --limit 20 \
    --sort revenue --order desc

# Filters-only search (JSON or repeated key=value)
$ handelsregister search \
    --filters '{"city":"München","ownership_filters":{"owner_managed":true}}' \
    --match-context

# Enrich a file
$ handelsregister enrich companies.csv --input csv \
    --query-properties name=company_name location=city \
    --snapshot-dir snapshots \
    --feature related_persons --feature financial_kpi \
    --output-format csv

# Download documents
$ handelsregister document "KONUX GmbH München" \
    --type shareholders_list --output konux_shareholders.pdf

$ handelsregister document "KONUX GmbH München" \
    --type articles_of_association --output konux_articles.pdf

$ handelsregister document "KONUX GmbH München" \
    --type SI --output konux_structured.xml

# Monitoring
$ handelsregister monitors pricing --interval 7
$ handelsregister monitors list
$ handelsregister monitors create --entity-id cc78cf0b230aeae35c6df7ba31989bb9 \
    --interval 7 --endpoint wep_01hzy2q6j3g5m8v9x0abcde123 \
    --label "BMW AG"
$ handelsregister monitors show mon_01hzy2q6j3g5m8v9x0abcde123
$ handelsregister monitors pause mon_01hzy2q6j3g5m8v9x0abcde123

# Webhook endpoints, deliveries, events
$ handelsregister webhooks create --name "Production receiver" \
    --url https://hooks.example.com/handelsregister --header x-tenant=customer-42
$ handelsregister webhooks verify wep_01hzy2q6j3g5m8v9x0abcde123
$ handelsregister webhooks deliveries --endpoint wep_01hzy2q6j3g5m8v9x0abcde123
$ handelsregister webhooks events
```

## Available Features (`fetch-organization`)

| Feature Flag                         | Description                                                   |
|--------------------------------------|---------------------------------------------------------------|
| `related_persons`                    | Current and past management                                   |
| `financial_kpi`                      | Yearly revenue, net income, employees, …                      |
| `balance_sheet_accounts`             | Hierarchical balance sheet data                               |
| `profit_and_loss_account`            | Profit & loss statements                                      |
| `annual_financial_statements`        | Full annual reports as Markdown                               |
| `annual_financial_statements__html`  | Full annual reports as HTML                                   |
| `publications`                       | Official Handelsregister publications                         |
| `insolvency_publications`            | Insolvency court publications                                 |
| `news`                               | News articles about the company                               |
| `website_content`                    | Company website as structured Markdown (AI mode, 0 credits)   |
| `shareholders` (beta)                | Shareholders with capital contribution and ratio              |
| `ubos` (beta)                        | Ultimate beneficial owners (resolved / unresolved / coverage) |
| `shareholdings` (beta)               | Outbound shareholdings (what the company owns in others)      |
| `mergers_and_acquisitions` (beta)     | M&A transactions, succession, agreements, and control         |
| `network` (beta, Pro/Max)             | Relationship graph of connected organizations and people     |

`network` costs 25 credits when it returns data, in addition to the 5-credit
organization lookup. It requires a Pro or Max plan.
When `network` is requested, the SDK checks the account's subscription through
the free Account API before making the billable organization request. Accounts
below Pro receive `SubscriptionRequiredError` with the accepted plans instead
of a silently reduced base profile.

`realtime_mode="handelsregister-default"` forces a live Handelsregister lookup (+10 credits), independent of the feature flags above.
It cannot be combined with `related_persons` or `publications`.

The base response includes `representation_scheme`. Entries in
`related_persons` may include both `organization_representation_scheme` and
`role_representation_scheme`. Historical person records can expose their last
applicable rules as `latest`; the SDK normalizes `current` and `latest` through
the `.active` property.

## `Company` properties

```python
# Basic
company.name
company.entity_id
company.status
company.is_active
company.purpose
company.representation_scheme          # RepresentationScheme

# Registration
company.registration_number
company.registration_court
company.registration_type
company.registration_date

# Contact & address
company.address
company.formatted_address
company.coordinates
company.website
company.phone_number
company.email

# Financial
company.financial_kpi
company.financial_years
company.balance_sheet_accounts
company.profit_and_loss_account
company.annual_financial_statements
company.annual_financial_statements_html
company.get_financial_kpi_for_year(2023)
company.get_balance_sheet_for_year(2023)
company.get_profit_and_loss_for_year(2023)
company.get_annual_financial_statement_for_year(2023)  # Markdown
company.get_annual_financial_statement_for_year(2023, html=True)

# People & ownership
company.current_related_persons
company.past_related_persons
company.get_related_persons_by_role("MANAGING_DIRECTOR")
company.related_person_entries         # typed persons + representation schemes
company.shareholders           # ShareholderInfo
company.ubos                   # UBOInfo
company.shareholdings          # ShareholdingsInfo
company.mergers_and_acquisitions  # MergersAndAcquisitions
company.network               # OrganizationNetwork

# News & publications
company.publications
company.insolvency_publications
company.news
company.website_content
```

## `Person` properties

```python
person.entity_id
person.name
person.canonical_name
person.given_name
person.family_name
person.maiden_name
person.previous_names
person.birth_date
person.home_city
person.home_location
person.bio
person.expertise
person.emails
person.phones
person.linkedin
person.github
person.other_profiles

person.handelsregister_roles
person.current_handelsregister_roles
person.get_handelsregister_roles_by_label("MANAGING_DIRECTOR")
person.affiliations

person.shareholdings           # PersonShareholdings (requires feature flag)
```

## Error handling

All API exceptions inherit from `HandelsregisterError`. Documented HTTP
responses are mapped to `RequestValidationError` (HTTP 400/422), `AuthenticationError`,
`InsufficientCreditsError`, `ForbiddenError` /
`SubscriptionRequiredError`, `NotFoundError`, `ConflictError` /
`IdempotencyConflictError` (HTTP 409), `IdempotencyKeyRequiredError`
(HTTP 428), `RateLimitError`, and `RequestTimeoutError` / `ServerError` /
`ServiceUnavailableError` (503 kill switch). Receiver-side signature
failures raise `WebhookSignatureError`. API exceptions preserve
`status_code`, the raw JSON `payload`, and billing metadata through `.meta`.
`SubscriptionRequiredError` additionally exposes `.required_plans`,
`.blocked_filters`, and `.blocked_features` when the API supplies them:

```python
from handelsregister import SubscriptionRequiredError

try:
    page = client.search_organizations(filters={
        "ownership_filters": {"owner_managed": True},
    })
except SubscriptionRequiredError as error:
    print(error)                 # Human-readable server explanation
    print(error.required_plans)  # e.g. ["pro", "max"]
    print(error.blocked_filters)
```

The command-line client renders these failures as a concise plan-required
message on stderr and exits with status 1; it does not print a Python traceback.

Only network failures, HTTP 408/429, and server errors are retried. When supplied,
the API's `Retry-After` header controls the delay. Monitoring mutations retry
with the same idempotency key; HTTP 409 is never retried, and endpoint
verify/test retry only the pre-operation 503 kill switch and HTTP 429
because other failures are ambiguous once the receiver may have been
contacted.

## Security

Do not commit API keys or Bearer tokens. Load credentials from environment
variables or a secret manager, and revoke any credential that may have been
exposed. Report vulnerabilities privately as described in
[SECURITY.md](SECURITY.md).

## License

GNU Affero General Public License v3.0 — see [LICENSE](LICENSE).
