Build a B2B company list in Excel with WZ filters and the API

5 min de citit

A bounded Python workflow for filtered company search, reliable identity matching, optional contact enrichment and Excel export, with checked API costs.

Acest articol este disponibil în engleză.

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.