B2B-Lead-Liste in Excel bauen: Mit WZ2025-Filter und Telefonnummern aus der handelsregister.ai API
Schritt-für-Schritt-Tutorial: Gefilterte B2B-Leadliste deutscher GmbHs mit WZ2025-Branche, Standort und Telefonnummern – via handelsregister.ai API und Export nach Excel.
Lead-Recherche im B2B-Sales sieht oft so aus: Excel-Tab auf, manuelle Suche in handelsregister.de, Copy-Paste der Firmendaten, dann LinkedIn auf für die Telefonnummer, dann Impressum auf, weil LinkedIn keine Nummer hatte. Pro Lead ~5–10 Minuten. Für eine Liste mit 200 Leads sind das gut zwei Arbeitstage – und am Ende fehlt bei einem Viertel der Einträge trotzdem die Telefonnummer.
Das hier ist die API-Variante davon: Filtern, anreichern, exportieren – als Python-Skript, ~50 Zeilen, ~3 Minuten Laufzeit für 200 Leads. Mit dem Nov-2025-Release der handelsregister.ai-Suche bekommst du echte Multi-Filter (Branche per WZ2025, Standort, Rechtsform, Mitarbeiterzahl), und für jede gefundene Firma lieferst du per ai_search=on-default die angereicherten Kontaktdaten inklusive Telefonnummer mit. Am Ende landet alles in einer sauberen .xlsx.
Der Workflow in vier Schritten
- Filter definieren – Branche (WZ2025-Code), Standort, Rechtsform, Größe
- Suche aufrufen –
/api/v1/search-organizationsliefert dir die gefilterte Treffermenge (paginiert) - Enrichment-Loop – für jeden Treffer
/api/v1/fetch-organizationmitai_search=on-defaultaufrufen, umcontact_datainklusive Telefonnummer zu bekommen - Excel-Export – pandas →
.xlsx, fertig fürs Vertriebs-Tool
Wichtig zu verstehen: Die Such-API liefert dir Stammdaten (Name, Adresse, HR-Nummer, Zweck) – aber keine Telefonnummern. Die stecken im vollen Firmenprofil und werden im AI-Modus zuverlässig aus den hinterlegten Quellen extrahiert. Deshalb brauchst du zwei API-Stufen.
Schritt 1: Filter definieren – WZ2025 als Branchen-Anker
Das Nov-2025-Release brachte echte Multi-Dimension-Filter in /api/v1/search-organizations. Du kannst kombinieren:
- Branche – WZ-Codes oder Branchenkategorien (parallel WZ2008 und neue WZ2025-Klassifikation, EU-aligned mit NACE Rev. 2.1)
- Standort – Bundesland, Stadt, PLZ-Bereich
- Rechtsform – GmbH, UG, AG, OHG, KG …
- Größe – Mitarbeiterzahl, Umsatzklassen
- Handelsregisternummer – direkter HRB/HRA-Lookup
WZ2025 ist dabei der eigentliche Gamechanger: Die alte WZ2008 hat moderne Geschäftsmodelle (SaaS, Plattform-Ökonomie, KI-Dienste) nur unsauber abgebildet. WZ2025 spiegelt die Realität deutlich besser, und weil beide Klassifikationen parallel zurückkommen, kannst du beide Welten bedienen – legacy CRM-Felder und das aktuelle Mapping.
Praxis-Beispiel: „Alle GmbHs im Münchener Innenstadt-PLZ-Anker, später verfeinert auf SaaS-/IT-Branche und Mitarbeiterzahl." Verifiziert dokumentiert ist aktuell der Filter postal_code – damit baust du den Standort-Anchor:
filters = {"postal_code": "80331"} # München-Innenstadt
Die übrigen Filter aus dem Nov-2025-Release (WZ-Code/Branchenkategorie, Stadt, Bundesland, Rechtsform, Mitarbeitende, Umsatzklasse, HR-Nummer) hängst du nach demselben Muster ans filters-Objekt – die exakten JSON-Schlüssel ziehst du dir am besten frisch aus der aktuellen API-Doku, da sich Filter-Schemas zwischen Releases noch bewegen können. Konzeptuelles Schema:
# Konzeptuell – exakte Keys gegen aktuelle Doku verifizieren:
filters_full = {
"postal_code": "80331",
# "wz_code": "62.01", # WZ2025-Code für Programmierung – illustrativ
# "legal_form": "GmbH",
# "employees_min": 50,
}
Konzeptionell läuft es immer auf „diese Filter-Dimension in dieses JSON-Feld" hinaus. Pragmatisch: wenn dein Use Case mehr als PLZ braucht, klär einmal die aktuellen Keys, lass das Skript danach unverändert.
Schritt 2: Suche aufrufen mit Pagination
/api/v1/search-organizations kostet 1 Credit pro Call und liefert bis zu 100 Treffer pro Seite. Für größere Listen iterierst du mit skip und limit:
import os, httpx, json
BASE = "https://handelsregister.ai/api/v1"
HEAD = {"x-api-key": os.environ["HANDELSREGISTER_API_KEY"]}
def search_all(filters: dict, query: str, page_size: int = 100):
skip, results = 0, []
while True:
params = {"q": query, "limit": page_size, "skip": skip,
"filters": json.dumps(filters)}
page = httpx.get(f"{BASE}/search-organizations", params=params, headers=HEAD).json()
hits = page.get("results", [])
results.extend(hits)
if len(hits) < page_size:
break
skip += page_size
return results
# `q` ist Pflicht mit min. 2 Zeichen – also einen sinnvollen breiten Begriff geben:
candidates = search_all(filters, query="GmbH")
print(f"{len(candidates)} Treffer")
Der Rückgabewert pro Treffer enthält entity_id, name, registration, address, registration_date, purpose – genug, um zu erkennen, welche Firmen du tatsächlich anreichern willst, bevor du Credits in die Vollabfrage steckst.
Tipp: Bevor du den Enrichment-Loop startest, sieb hier nochmal serverseitig oder lokal – z. B. Firmen, deren purpose „Vermögensverwaltung" enthält, sind selten echte Sales-Leads.
Schritt 3: Enrichment-Loop mit ai_search=on-default für Telefonnummern
Jetzt der eigentliche Mehrwert. Pro Kandidat ein Aufruf auf /api/v1/fetch-organization mit aktiviertem AI-Modus – der zieht aus den offiziellen Quellen plus der Firmenwebsite die strukturierten Kontaktdaten:
import time
def enrich(name: str, city: str | None) -> dict:
# Name + Stadt für die Disambiguierung – sonst kann q=name den falschen Treffer ziehen
q = f"{name} {city}" if city else name
params = {"q": q, "ai_search": "on-default"}
r = httpx.get(f"{BASE}/fetch-organization", params=params, headers=HEAD)
if r.status_code == 429:
time.sleep(2); return enrich(name, city)
data = r.json()
contact = data.get("contact_data", {}) or {}
return {
"name": data.get("name"),
"legal_form": data.get("legal_form"),
"hr": f"{data['registration']['register_type']} {data['registration']['register_number']} ({data['registration']['court']})",
"city": data.get("address", {}).get("city"),
"postal_code": data.get("address", {}).get("postal_code"),
"phone": contact.get("phone_number"),
"website": contact.get("website"),
"purpose": data.get("purpose"),
}
enriched = [enrich(c["name"], c.get("address", {}).get("city")) for c in candidates]
Was kommt zurück: contact_data.phone_number und contact_data.website – die zwei Felder, die manuelle Lead-Recherche so zäh machen. Der AI-Modus extrahiert die strukturiert, statt dass du selbst HTML parsen musst.
Wenn du noch tiefer willst (z. B. den Gesamtinhalt der Firmenwebsite als Markdown für eine LLM-Pipeline): hänge feature=website_content dran – im AI-Modus kostet das 0 Credits zusätzlich.
Schritt 4: Export nach Excel
Pandas plus openpyxl als Engine – zwei Zeilen:
import pandas as pd
df = pd.DataFrame(enriched)
df.to_excel("leads.xlsx", index=False, sheet_name="Leads")
Optional schöner machen: Spaltenbreiten setzen, Header fett, Filter aktivieren. Drei Zeilen mehr mit openpyxl direkt – aber für 95 % der Sales-Workflows reicht die obige Variante.
End-to-End-Skript
Komplette Pipeline, ~50 Zeilen:
import os, time, json, httpx, pandas as pd
BASE = "https://handelsregister.ai/api/v1"
HEAD = {"x-api-key": os.environ["HANDELSREGISTER_API_KEY"]}
# filters: aktuell dokumentiert ist postal_code; weitere Dimensionen
# (WZ-Code, Rechtsform, Mitarbeiter, …) ergänzen, sobald die Keys aus
# der aktuellen API-Doku geprüft sind.
filters = {"postal_code": "80331"}
def search_all(filters, query, page_size=100):
skip, results = 0, []
while True:
params = {"q": query, "limit": page_size, "skip": skip,
"filters": json.dumps(filters)}
hits = httpx.get(f"{BASE}/search-organizations",
params=params, headers=HEAD).json().get("results", [])
results.extend(hits)
if len(hits) < page_size: break
skip += page_size
return results
def enrich(name, city):
q = f"{name} {city}" if city else name
r = httpx.get(f"{BASE}/fetch-organization",
params={"q": q, "ai_search": "on-default"}, headers=HEAD)
if r.status_code == 429: time.sleep(2); return enrich(name, city)
d = r.json(); c = d.get("contact_data") or {}; a = d.get("address") or {}
reg = d.get("registration") or {}
return {"name": d.get("name"), "legal_form": d.get("legal_form"),
"hr": f"{reg.get('register_type')} {reg.get('register_number')} ({reg.get('court')})",
"city": a.get("city"), "postal_code": a.get("postal_code"),
"phone": c.get("phone_number"), "website": c.get("website"),
"purpose": d.get("purpose")}
cands = search_all(filters, query="GmbH")
df = pd.DataFrame([enrich(c["name"], c.get("address", {}).get("city")) for c in cands])
df.to_excel("leads.xlsx", index=False, sheet_name="Leads")
print(f"{len(df)} Leads exportiert, davon {df['phone'].notna().sum()} mit Telefon.")
Speichern als build_leads.py, export HANDELSREGISTER_API_KEY=..., python build_leads.py – fertig.
Kosten realistisch kalkulieren
Pro Lead-Pipeline:
- Suche: 1 Credit pro Seite (à 100 Treffer)
- Enrichment: 5 Credits pro Firma (Basis-Fetch,
ai_search=on-defaultist Standard, ohne zusätzliche Features)
Beispielrechnungen:
- 100 Leads → 1 Suche + 100 × 5 = 501 Credits
- 500 Leads → 5 Suchen + 500 × 5 = 2.505 Credits
- 2.000 Leads → 20 Suchen + 2.000 × 5 = 10.020 Credits
Plus-Plan (69 €/Monat, monatlich erneut aufgefüllt) deckt typische Mid-Market-Pipelines locker ab; für hochfrequentes Lead-Mining lohnt sich der Pro-Plan.
Edge Cases und DSGVO-Hygiene
- Fehlende Telefonnummer. Nicht jede Firma hinterlegt eine zentrale Nummer; die Coverage variiert je nach Branche und Firmengröße deutlich. Plane einen Fallback („Hauptadresse zur postalischen Erstansprache") und messe deine echte Trefferquote nach den ersten 100 Leads.
- Rate Limits. 60 Calls/min. Bei 2.000 Leads kommt der Loop in Rate-Limit-Bereiche – baue Backoff ein (siehe
429-Branch oben) und ggf. asyncio mit Semaphore für höhere Durchsätze. - Dubletten. Bei breiteren Filtern landen manchmal Tochter- und Holding-GmbHs derselben Gruppe in der Liste. Dedup über
hr(Registernummer + Gericht) ist ein zuverlässiger Anker. - DSGVO. Telefonnummern und Adressen sind personenbezogen, wenn sie individualisierbar sind. Für B2B-Coldcall-Ansprache stützt sich der typische Use Case auf Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse) plus § 7 UWG (Telefonwerbung nur mit mutmaßlicher Einwilligung im B2B-Kontext). Mach die Interessenabwägung schriftlich, dokumentiere die Datenquelle, und respektiere Widersprüche.
- Aktualität. Firmendaten ändern sich. Setze ein TTL von 30–60 Tagen, danach wieder durch den Enrichment-Schritt laufen lassen.
Für nicht-Entwickler: handelsregister.ai bietet zusätzlich einen CSV-Enricher als App – du lädst eine bestehende Liste mit Firmennamen hoch, bekommst die angereicherte Variante zurück. Das passende Komplement zu diesem Skript, wenn deine Quelle nicht der Filter-Search ist, sondern z. B. eine bestehende CRM-Liste.
Recap
Eine sauber gefilterte B2B-Leadliste mit Telefonnummern braucht heute kein Excel-Massaker mehr: WZ2025-Filter in der Suche, AI-Modus auf der Vollabfrage, pandas auf den Export. Die zwei API-Stufen sind der ganze Trick – Suche liefert dir die richtigen Firmen, Enrichment liefert dir die anrufbaren Kontaktdaten.
Probiere es mit deinem nächsten Sales-Sprint: Definiere zwei Filter (Branche + Standort), lass das Skript 100 Kandidaten ziehen, schau dir die Phone-Coverage an und vergleiche mit deiner sonstigen Lead-Quelle. Innerhalb einer Stunde weißt du, ob das die Quelle für deine nächste Q-Pipeline wird.