# Steuerberaterverzeichnis Scraper - German Tax Adviser Leads (`scrapersdelight/steuerberaterverzeichnis-scraper`) Actor

German tax advisers (Steuerberater) and tax firms from the official BStBK register: email, phone, website, office address, Safe ID and chamber. Search by postcode, city, name, chamber or all of Germany. From $4 per 1,000 records.

- **URL**: https://apify.com/scrapersdelight/steuerberaterverzeichnis-scraper.md
- **Developed by:** [Scrapers Delight](https://apify.com/scrapersdelight) (community)
- **Categories:** Lead generation, Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$4.00 / 1,000 per tax adviser records

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Steuerberaterverzeichnis Scraper — German Tax Adviser Leads with Email & Phone

Export **German tax advisers (Steuerberater), Steuerbevollmächtigte and tax-advisory firms
(Steuerberatungsgesellschaften)** from the **official Steuerberaterverzeichnis** kept by the
Bundessteuerberaterkammer (BStBK) — with the **business email, phone, mobile, website and office
address** each member publishes there. Search by **postcode, city, name,
regional chamber, or the whole of Germany**.

- ✉️ **Email on 97.5% of records**, phone or mobile on 82%, website on 28% (measured on 17,882 live records — see the per-chamber table below)
- 🏛️ **Official source**: the Bundessteuerberaterkammer's register of every German tax adviser
- 🗺️ **Complete coverage**: the register refuses broad searches ("Zu viele mögliche Treffer"); this actor narrows every refused search digit by digit and **checks its row count against the register's own count** for every cell
- 🔑 **Safe ID** (beSt electronic mailbox id) on every record, plus appointment date, chamber, firm management and partners
- 💶 **$4 per 1,000 records**, charged only for records delivered — no start fee, no charge for filtered-out or duplicate records

### Who uses this

- **Tax & accounting software vendors** (DATEV alternatives, bookkeeping, payroll, e-invoicing / XRechnung tools) building a partner or sales pipeline of every Kanzlei in a region
- **Legal-tech, fintech and HR/payroll partner programmes** recruiting Steuerberater as channel partners
- **Agencies** feeding Kanzlei addresses into enrichment and CRM workflows
- **Compliance teams** verifying that an adviser is registered (and with which chamber) before relying on them

### What one record looks like

Real record from a verified run (Hamburg, 2026-09-23):

```json
{
  "registerId": "44-DB-BC-92-53-1E-60-3A-30-48-4D-AF-87-B1-C3-71",
  "entityType": "COMPANY",
  "name": "Jacobsen + Confurius Rechtsanwälte Steuerberater Wirtschaftsprüfer Partnerschaftsgesellschaft mbB",
  "profession": null,
  "academicTitles": null,
  "legalForm": "PartG mbB",
  "addressAddition": null,
  "street": "Pinnasberg 47",
  "postalCode": "20359",
  "city": "Hamburg",
  "address": "Pinnasberg 47, 20359 Hamburg",
  "email": "info@jacobsen-confurius.de",
  "phone": "040 3020030",
  "mobile": null,
  "fax": "040 337820",
  "website": "www.jacobsen-confurius.de",
  "websiteUrl": "http://www.jacobsen-confurius.de",
  "hasEmail": true,
  "hasPhone": true,
  "hasContact": true,
  "safeId": "DE.BStBK.8f5d8ca5-41b6-44ad-be2d-92e61f2d80fd.2709",
  "dateType": "Anerkennungsdatum",
  "date": "2022-12-06",
  "chamber": "Hamburg",
  "chamberAsPublished": "Hamburg",
  "chamberAddress": "Kurze Mühren 3, 20095 Hamburg",
  "otherOffices": ["Eisenzahnstr. 64, 10709 Berlin-Wilmersdorf"],
  "foreignSeat": null,
  "foreignPrincipalPlace": null,
  "managementBody": [
    { "name": "Jens Borchardt", "profession": "Rechtsanwalt" },
    { "name": "Manfred Confurius", "profession": null }
  ],
  "authorizedPartners": [ { "name": "Manfred Confurius", "profession": null } ],
  "partners": [],
  "practiceRepresentatives": [],
  "detailUrl": "https://steuerberaterverzeichnis.berufs-org.de/details/44-DB-BC-92-53-1E-60-3A-30-48-4D-AF-87-B1-C3-71/?lang=de",
  "scrapedAt": "2026-09-23T04:24:55.119Z"
}
```

(The management and partner lists are shortened here; the real record lists all 15 and 11 names.)
An individual adviser looks the same with `entityType: "PERSON"`, a `profession` such as
`Steuerberater` / `Steuerberaterin` / `Steuerbevollmächtigter`, `academicTitles` such as `Dr.`, and —
for employed advisers — the firm they work at in `addressAddition`.

### Every field

| Field | What it is |
|---|---|
| `registerId` | The register's own record id. Stable across runs; the dedupe key — no record is delivered or charged twice in a run |
| `entityType` | `PERSON` (Steuerberater/in, Steuerbevollmächtigte/r) or `COMPANY` (Berufsausübungsgesellschaft) |
| `name` | Full name of the adviser, or the firm name |
| `profession` | Berufsbezeichnung, e.g. Steuerberater, Steuerberaterin, Steuerbevollmächtigter (people only) |
| `academicTitles` | e.g. `Dr.`, `Dipl.-Kaufmann`, as printed |
| `legalForm` | Firms only: GmbH, PartG mbB, GbR, … |
| `addressAddition` | Lines above the street — usually the firm an employed adviser works at |
| `street`, `postalCode`, `city`, `address` | Office (berufliche Niederlassung) address |
| `email` | Business email |
| `phone`, `mobile`, `fax` | Business numbers, exactly as printed |
| `website`, `websiteUrl` | Website as the member wrote it, and the register's own clickable form of it |
| `hasEmail`, `hasPhone`, `hasContact` | Booleans for fast filtering in a spreadsheet |
| `safeId` | Safe ID of the besonderes elektronisches Steuerberaterpostfach (beSt) |
| `dateType`, `date` | Which date the register gives (Bestelldatum = appointment, Anerkennungsdatum = firm recognition, Registrierungsdatum) and the date itself, ISO |
| `chamber`, `chamberAsPublished`, `chamberAddress` | The responsible Steuerberaterkammer (one of 21) |
| `otherOffices` | A firm's further advisory offices (Weitere Beratungsstellen) |
| `foreignSeat`, `foreignPrincipalPlace` | Foreign firms only: country of seat and town of principal establishment |
| `managementBody`, `authorizedPartners`, `partners` | Firms: named directors, representing partners and shareholders, each with profession where printed |
| `practiceRepresentatives` | An adviser's appointed Praxisvertreter: name and address |
| `detailUrl` | The record in the official register |
| `scrapedAt` | When this run read the record |

### Contact fill — measured, per chamber

Measured on **17,882 distinct records** from five live runs on 2026-09-23 (postcode areas 0xxxx,
1xxxx, 20xxx, 80xxx, 657xx, 48143, 39104). "Phone" = landline; "phone or mobile" = either.

| Chamber | Records | Email | Phone | Phone or mobile | Website | Any contact |
|---|---|---|---|---|---|---|
| Berlin | 4,557 | 97.5% | 84.7% | 94.0% | 33.8% | 99.0% |
| München | 3,415 | 96.4% | 82.2% | 94.9% | 23.7% | 98.7% |
| Sachsen | 3,141 | 97.0% | 59.2% | 63.4% | 37.0% | 98.0% |
| Hamburg | 2,658 | 99.7% | 48.8% | 57.7% | 12.0% | 99.9% |
| Brandenburg | 1,478 | 99.7% | 74.6% | 91.9% | 28.5% | 99.9% |
| Mecklenburg-Vorpommern | 934 | 94.5% | 89.1% | 96.6% | 22.9% | 98.4% |
| Sachsen-Anhalt | 678 | 97.8% | 90.7% | 95.0% | 47.5% | 99.1% |
| Hessen | 471 | 97.2% | 50.7% | 58.4% | 23.1% | 98.3% |
| Thüringen | 376 | 99.5% | 92.6% | 94.4% | 28.2% | 99.5% |
| Westfalen-Lippe | 117 | 86.3% | 53.0% | 61.5% | 7.7% | 89.7% |
| **All 17,882** | | **97.5%** | **73.0%** | **82.1%** | **28.1%** | **98.8%** |

Chambers not in the table (Düsseldorf, Köln, Niedersachsen, Nordbaden, Nürnberg, Rheinland-Pfalz,
Saarland, Schleswig-Holstein, Stuttgart, Südbaden, Bremen) had fewer than 100 records in these
runs; a second, smaller frame of 758 records across seven chambers (incl. Bremen 150, Westfalen-Lippe
116\) read before the build showed the same shape: email 86–100%, phone 39–95%, website 8–41% per
chamber. How often the phone field is filled is a property of the chamber, not of this actor.

Each chamber feeds the register automatically. A missing phone or website means the adviser did not give
one to their chamber — not a parsing gap: on every run, `RUN_SUMMARY.labelPresentToValueEmitted`
counts how often the register printed each label against how often a value was emitted.

### Input

| Input | What it does |
|---|---|
| `postcodes` | 1–5 digit postcodes or prefixes. `80331` = one postcode; `803` = all of 803xx; `8` = all of 8xxxx |
| `cities` | Town names (`Münster`, `Frankfurt am Main`) — the register matches the start of the name |
| `names` | Surnames or firm names, `*` / `?` wildcards allowed (`Mül*`) |
| `allGermany` | The whole register (109,109 records on 2026-09-23). Ignores the three lists above. Paced by the register's quota — see *Big jobs* below |
| `chamber` | Keep only one of the 21 Steuerberaterkammern. Applied on the register's side where possible, so a region with no member of that chamber costs no record requests |
| `entityType` | `ALL`, `PERSON` (individual advisers) or `COMPANY` (firms). Decided from the result list, so it costs nothing extra |
| `requireEmail` / `requireContact` | Deliver (and charge for) only records with an email / with any contact |
| `maxResults` | Stop after this many delivered records (default 80; `0` = no cap) |
| `detailConcurrency` | Parallel requests (default 8). The actor halves it automatically when the register answers HTTP 429 |
| `residentialFallback` | If the register blocks this run for a minute or more (its limit is shared with other jobs), continue on Apify RESIDENTIAL proxy instead of waiting. Default on; no effect on price |
| `proxyConfiguration` | Off by default: the register has no anti-bot and answers Apify's servers directly |

Leave everything empty to run the small demo scope (postcode 65719, Hofheim am Taunus).

#### Examples

```json
{ "postcodes": ["80331", "80333", "80335"], "maxResults": 0 }
```

Every tax adviser in central Munich.

```json
{ "allGermany": true, "chamber": "Westfalen-Lippe", "requireEmail": true, "maxResults": 0 }
```

Every member of the Steuerberaterkammer Westfalen-Lippe that publishes an email.

```json
{ "cities": ["Leipzig"], "entityType": "COMPANY", "maxResults": 500 }
```

Tax-advisory firms in Leipzig, with their directors and partners.

### Pricing

**$0.004 per record delivered ($4 per 1,000).** One event, `advisor-record`, charged only when a
record lands in your dataset. **Not charged:** records removed by your `chamber` / `entityType` /
`requireEmail` / `requireContact` filters, a record already delivered in the same run, searches
that match nobody, or anything that could not be fetched. No start fee.

| Job | Records | Cost |
|---|---|---|
| Demo scope (empty input) | 80 | $0.32 |
| All of Hamburg 20xxx | 2,656 | $10.62 |
| Munich 80xxx + Berlin 10xxx | 6,135 | $24.54 |
| The whole register | 109,109 | $436.44 |

Set **Max results** or Apify's **maximum cost per run** to cap a job; the actor stops cleanly at
either and never delivers a record it could not charge for.

### How it works (and why the count is right)

1. The register answers a search with **every** matching record on one page ("Treffer: 671 von 671") — or refuses it outright as too broad. A refused search is narrowed along the postcode: `8??` → `80*` → `803` → `8033` → `80331`. A full 5-digit postcode has always been answered (largest seen: 10117 Berlin, 671 records).
2. For every answered cell the actor **floor-asserts** the rows it parsed against the register's own count. The register's ordering is not deterministic on big cells — the same request for 20354 returned 660, 659 and 660 distinct records under an unchanged "661 von 661" — so a short cell is asked again and the answers are **unioned by register id** until they reach the register's count. Cells that never reach it are listed in `RUN_SUMMARY`.
3. Each record's page is opened for the contact data (the result list carries only name and address), deduplicated by `registerId` **before** charging.
4. `RUN_SUMMARY` keeps the kinds of nothing apart: cells the register answered with no records, searches it refused as invalid, cells it refused as too broad (and how they were narrowed), cells whose count and rows disagreed, and records that stayed unreachable. None of them are charged.

**Verified completeness:** an independent census walk of every postcode cell on 2026-09-23 counted
**109,109 register rows** (109,103 distinct ids — the six-record gap is the non-deterministic
ordering above). The live actor matched the register's count exactly on every area checked:
80xxx + 10xxx **6,135 = 6,135**, 1xxxx **6,802 = 6,802**.

#### Big jobs and the register's rate limit

The register answers `429 Zu viele Anfragen` with a `Retry-After` once a client group has sent
roughly **8,900 requests** in a short window, and knocking during that block makes it longer. The
quota is **shared**: ten parallel runs on ten different Apify IPs were blocked in the same second,
so running several jobs at once is not faster than one. The actor therefore spends a burst of 6,000
requests at full speed — a city, a postcode area or a whole chamber's region of up to ~5,500 records
finishes inside it, in minutes — and then paces itself at 5 requests per second, honouring any
`Retry-After` in full and pausing **every** request while it lasts. A record never costs more
because of this; a big job just takes longer: the whole register (≈116,000 requests) takes about
**6–7 hours in one run** (the default run timeout is 8 hours). If a search area still cannot be
reached, the run ends **FAILED with the areas named** rather than reporting a partial export as
complete — and the records it did deliver are real and were charged.

### Honest limits

- **Contact is only what the adviser gave their chamber.** About 4% of records have no email; websites are published by roughly a quarter. The actor never guesses a domain or completes a number.
- **Malformed entries are not repaired.** The register prints a few emails like `x@firm,de`, `x@me-com` or `x@firm .de`; those are not emitted (they are listed in `RUN_SUMMARY.emailsRejectedAsMalformed`). A pasted `E-Mail:` label or a trailing full stop *around* a valid address is trimmed; umlaut domains (`…@steuerkanzlei-hübner.de`) are real and are kept.
- **Sachsen-Anhalt** is printed by the register only as "Körperschaft des öffentlichen Rechts". `chamber` resolves it to `Sachsen-Anhalt` from the chamber's own Magdeburg address; `chamberAsPublished` keeps the printed text.
- **Professional bans (Berufs-/Vertretungsverbote)** are part of the register entry, but none was printed on any record we read, so there is no field for them yet. If the register prints one, it appears in `RUN_SUMMARY.rareNoticeBlocks` rather than being dropped.
- **People in firm lists** (`managementBody`, `partners`) carry a profession only where the register prints one.
- The register rate-limits (see *Big jobs*). The actor backs off run-wide and never keeps knocking during a block; a whole-Germany export therefore takes hours, and runs started in parallel share the same quota.

### Source

The Steuerberaterverzeichnis of the Bundessteuerberaterkammer, <https://steuerberaterverzeichnis.berufs-org.de/>.

### FAQ

**Can I get every tax adviser in Germany?** Yes — `allGermany: true`, `maxResults: 0`, in ONE run (about 6–7 hours, paced by the register's quota; parallel runs share that quota and are not faster). 512 MB is enough (peak measured: 296 MB on 6,135 records).

**Why not a Google Maps or Gelbe Seiten scrape?** Those list whoever bought a listing. This is the official register: every appointed Steuerberater is in it, with the email they registered with their chamber, and nobody else is.

**How fresh is it?** Each run reads the live register; `scrapedAt` is on every record.

**Something looks wrong?** Open an issue with the run id. `RUN_SUMMARY` holds what the actor saw.

# Actor input Schema

## `postcodes` (type: `array`):

German postcodes to search, one per line. A full 5-digit PLZ (`80331`) returns every tax adviser registered there. A shorter prefix covers a whole area: `803` = all of 803xx, `80` = all of 80xxx, `8` = all of 8xxxx. The register refuses broad searches with "Zu viele mögliche Treffer"; this actor narrows a refused prefix digit by digit automatically, so a prefix of any length is complete. Leave everything empty to run the small demo scope (PLZ 65719, Hofheim am Taunus).

## `cities` (type: `array`):

Town names as the register writes them, e.g. `Hofheim`, `Münster`, `Frankfurt am Main`. The register matches the start of the town name. A big city is refused as too broad and narrowed by postcode automatically.

## `names` (type: `array`):

Search by surname (`Schmidt`) or by the name of a tax-advisory firm (`Main - Taunus`). At least 2 characters. `*` and `?` work as wildcards (`Mül*`). A common surname is refused as too broad and narrowed by postcode automatically.

## `allGermany` (type: `boolean`):

Walk the entire register, postcode area by postcode area. Ignores postcodes, cities and names. Combine with a chamber below to export one Steuerberaterkammer. Set Max results to 0 for no cap, and note that every delivered record is charged.

## `chamber` (type: `string`):

Keep only members of one of the 21 regional chambers. Applied on the register's side where possible: an area with no member of this chamber is skipped without opening a single record.

## `entityType` (type: `string`):

PERSON = individual Steuerberater/innen and Steuerbevollmächtigte. COMPANY = Berufsausübungsgesellschaften (Steuerberatungsgesellschaften, partnerships). ALL = both.

## `requireEmail` (type: `boolean`):

Skip (and never charge for) records that publish no valid email address.

## `requireContact` (type: `boolean`):

Skip (and never charge for) records that publish no email, phone, mobile or website.

## `maxResults` (type: `integer`):

Stop after this many records are delivered. Each delivered record is one charged event. 0 = no cap.

## `detailConcurrency` (type: `integer`):

How many record pages to open at once. The default is measured to be fast without straining the register.

## `residentialFallback` (type: `boolean`):

The register's rate limit is shared by all Apify servers, so another job can use it up. When the register blocks this run for a minute or more, continue on Apify RESIDENTIAL proxy instead of waiting (up to 30 minutes). No effect on price.

## `proxyConfiguration` (type: `object`):

The register has no anti-bot and answers Apify's own servers directly, so no proxy is used by default (measured: 651 records in 19 s, zero retries). Switch on Apify Proxy (RESIDENTIAL) or your own proxy if you prefer.

## Actor input object example

```json
{
  "postcodes": [
    "65719"
  ],
  "allGermany": false,
  "chamber": "",
  "entityType": "ALL",
  "requireEmail": false,
  "requireContact": false,
  "maxResults": 80,
  "detailConcurrency": 8,
  "residentialFallback": true,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `records` (type: `string`):

Name, profession, office address, email, phone, mobile, fax, website, Safe ID, appointment/recognition date, responsible chamber, other offices and (for firms) management and partners.

## `runSummary` (type: `string`):

Coverage per search, contact fill per chamber, label-present to value-emitted counts, the kinds of nothing (empty, refused, unreachable), and delivered == charged reconciliation.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "postcodes": [
        "65719"
    ],
    "maxResults": 80,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/steuerberaterverzeichnis-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "postcodes": ["65719"],
    "maxResults": 80,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/steuerberaterverzeichnis-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "postcodes": [
    "65719"
  ],
  "maxResults": 80,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call scrapersdelight/steuerberaterverzeichnis-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapersdelight/steuerberaterverzeichnis-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/eRhHRWCHYdDmVds7C/builds/3nZjuaRxwg2NGBDVY/openapi.json
