Build a B2B company list in Excel with WZ filters and the API
A bounded Python workflow for filtered company search, reliable identity matching, optional contact enrichment and Excel export, with checked API costs.
You can build a German B2B company list by filtering the handelsregister.ai search API, enriching selected company IDs and exporting the results to Excel. A search term is optional when you supply suitable filters. Phone numbers and websites may be available in an enriched profile, but neither is guaranteed for every company.
This walkthrough starts with ten companies. The WZ-filtered search and one subsequent enriched profile request were checked against the live API on 5 September 2026. The checks do not establish a guaranteed runtime or contact coverage for a larger dataset.
Choose location and industry filters
The example uses the documented filter keys city, industry_code and industry_scheme:
{
"city": "München",
"industry_code": ["62.10.1", "62.10.3"],
"industry_scheme": "WZ2025"
}
Use codes from the classification you specify. WZ 2008 and WZ 2025 codes are not interchangeable without checking their meaning. See the classification guide and the current search reference.
Financial ranges belong under financial_filters; for example, {"financial_filters":{"pl_revenue":{"gte":1000000}}}. Companies with missing financial data may not satisfy such a filter. Treat the output as a filtered dataset, not a complete census of every potentially suitable business.
Match companies by ID
Search returns entries in results, with an entity_id for each company. Pass that ID as q to fetch-organization and verify the returned ID before accepting the profile. This avoids silently selecting a different company through a second name-only search.
The script uses small pages, a company limit and bounded retries. When search returns total_exact: false, its total value should not be presented as an exact count of matching companies.
Run a bounded Python export
Install requests, pandas and openpyxl with pip install requests pandas openpyxl. Provide your key through the HANDELSREGISTER_API_KEY environment variable. The code sends it in a header, never in a URL.
import json
import os
import time
import pandas as pd
import requests
BASE = 'https://handelsregister.ai/api/v1'
FILTERS = {
'city': 'München',
'industry_code': ['62.10.1', '62.10.3'],
'industry_scheme': 'WZ2025',
}
MAX_COMPANIES = 10 # Start small: enriched profiles can cost 25 credits each.
PAGE_SIZE = 10
def get_json(session, endpoint, params):
for attempt in range(4):
response = session.get(BASE + endpoint, params=params, timeout=120)
if response.status_code == 429 and attempt < 3:
time.sleep(min(2 ** (attempt + 1), 30))
continue
response.raise_for_status()
return response.json()
raise RuntimeError('Request did not complete')
def find_companies(session, maximum=MAX_COMPANIES):
selected, seen = [], set()
skip = 0
for _ in range(5): # Cap search pages even if a page repeats.
if len(selected) >= maximum:
break
limit = min(PAGE_SIZE, maximum - len(selected))
page = get_json(session, '/search-organizations', {
'filters': json.dumps(FILTERS), 'limit': limit, 'skip': skip,
})
hits = page.get('results') or []
if not hits:
break
for hit in hits:
entity_id = hit.get('entity_id')
if entity_id and entity_id not in seen:
seen.add(entity_id)
selected.append(hit)
skip += len(hits)
if len(hits) < limit:
break
return selected[:maximum]
def excel_safe(value):
# Treat company-supplied text as text, not as an Excel formula.
if isinstance(value, str) and value.lstrip().startswith(('=', '+', '-', '@')):
return "'" + value
return value
def build_rows(session, candidates):
rows = []
for candidate in candidates:
entity_id = candidate['entity_id']
profile = get_json(session, '/fetch-organization', {
'q': entity_id, 'ai_search': 'on-default',
})
if profile.get('entity_id') != entity_id:
raise ValueError('Returned company does not match the selected entity_id')
address = profile.get('address') or {}
registration = profile.get('registration') or {}
contact = profile.get('contact_data') or {}
row = {
'entity_id': entity_id,
'name': profile.get('name'),
'register_type': registration.get('register_type'),
'register_number': registration.get('register_number'),
'court': registration.get('court'),
'city': address.get('city'),
'postal_code': address.get('postal_code'),
'phone': contact.get('phone_number'),
'website': contact.get('website'),
}
rows.append({key: excel_safe(value) for key, value in row.items()})
return rows
def main():
with requests.Session() as session:
session.headers.update({'x-api-key': os.environ['HANDELSREGISTER_API_KEY']})
rows = build_rows(session, find_companies(session))
if not rows:
print('No matching companies; no file exported.')
return
pd.DataFrame(rows).to_excel('leads.xlsx', index=False, sheet_name='Companies')
print(f'{len(rows)} companies exported; missing contact fields remain empty.')
if __name__ == '__main__':
main()
Save the code as build_leads.py and run python build_leads.py. Inspect the small initial batch before increasing MAX_COMPANIES. An unresolved HTTP error stops the export rather than silently representing a partial result as a complete list.
Contact values are read from contact_data when available. Missing values remain empty. The Excel text protection prevents company-supplied text beginning with a formula character from being interpreted as a formula.
Budget for the actual request configuration
The live checks on 5 September 2026 returned these costs in meta.request_credit_cost:
| Request | Credits in the successful test |
|---|---|
| Filtered company search | 1 |
Company profile with ai_search=on-default, no extra features |
25 |
The enriched request includes a 5-credit base charge and a 20-credit AI surcharge. Ten such profiles and one search page would therefore cost 251 credits under these conditions. This is a worked example, not a universal price: additional features, modes and retries can affect the bill. Check the fetch reference and your response metadata.
Validate the list before using it
Keep company IDs and register details, record retrieval dates, and review a sample of websites and phone numbers against the intended legal entities. A listed contact is not automatically a qualified lead or permission to contact someone. Check the rules for your chosen outreach channel, including section 7 UWG, and applicable data-protection requirements.
For an existing company list, consider the CSV-Enricher. For individual investigations, see the Explorer guide. For a repeatable automation, continue with the n8n integration guide.