# Vermittlerregister Scraper - German Insurance Broker Lookup (`scrapersdelight/vermittlerregister-de-scraper`) Actor

Look up German insurance brokers, tied agents and financial-investment intermediaries in the statutory Vermittlerregister (DIHK). Turn a list of company names, people or registration numbers into full permit records: GewO paragraph, permit scope, named directors and the supervising IHK.

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

## Pricing

from $4.00 / 1,000 per full register 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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## 🇩🇪 Vermittlerregister Scraper — German insurance broker & intermediary lookup

**Turn a list of names into statutory permit records.** Feed this actor German company names,
people (`Surname, Firstname`) or registration numbers, and it returns each one's entry in the
**Vermittlerregister** — the public register the DIHK runs for Germany's 79 chambers of commerce
under §§ 34d / 34f / 34h / 34i GewO.

Every row carries the layer no company-data source has: **which permit the intermediary actually
holds, decoded to its paragraph of the Gewerbeordnung**, what that permit covers, the **named**
authorised representatives and senior staff, and the licensing authority and IHK that supervise
them.

> ### ⚠️ Read this before you buy: this is a **batch lookup**, not a bulk export
>
> The register publishes no list. There is **no listing route, no page parameter** (a `seite=2`
> query returns byte-identical bytes to page 1 — measured), and any query it considers too broad
> comes back **`TOO_MANY_HITS` with zero rows**. Even a full German name like *Peter Müller* is
> refused. The site says so in its own words, which this actor quotes back to you verbatim on
> every refused row:
>
> > *„Ihre Suche hat zu viele Treffer ergeben. Bitte grenzen Sie die Suche weiter ein oder
> > verwenden Sie eine Registrierungsnummer als Suchkriterium."*
>
> So: **bring a list.** This actor is built for verifying, enriching and monitoring intermediaries
> you can already name. It cannot and does not dump the register.

***

### 🎯 What you get per row

| | |
|---|---|
| 🆔 | **Registration number** — `D-F-141-WS31-91`, `D-URTZ-05QX4-94`, `D-W-139-JLDM-77` |
| 📜 | **Permit type, decoded** — *„Versicherungsmakler mit Erlaubnis nach § 34d Abs. 1 GewO"*, *„Gebundener Versicherungsvertreter nach § 34d Abs. 7 GewO"*, *„Honorar-Finanzanlagenberater"* … |
| ⚖️ | **GewO paragraph** as its own field — § 34d Abs. 1 / Abs. 2 / Abs. 6 / Abs. 7, § 34f, § 34h, § 34i, § 34c |
| 🧾 | **Permit scope** — the three Erlaubnisumfang booleans (open-ended funds, closed-ended funds, other investments) plus their statutory wording |
| 🏢 | **Entity** — company name, or the sole trader's full name with salutation, titles and affixes |
| 📍 | **Business address** — street, house number, postcode, town, district |
| 👤 | **Named people** — `vertretungsberechtigte` (directors/board, with their function), `leitendeAngestellte` (§ 34d Abs. 10 GewO), `unmittelbar Mitwirkende`, managing partnerships |
| 🏛️ | **Licensing authority** — the town or district office that issued the permit, with its address, and whether the permit is under review |
| 🏦 | **Supervising IHK** — the chamber responsible for the entry, with its address and contact fields |
| 🔗 | **Cross-register links** — the same intermediary's entries in the other sub-registers (optional; see the note under *Repeat runs and the record cache*) |
| 📅 | **`letztesUpdate`** — the date the register itself published this data |
| 🧭 | **Provenance** — the lookup that produced the row, whether it was narrowed, whether it is complete, and the register's own search URL |

#### The four sub-registers, all searched in one request

| Code | Register | Statute |
|---|---|---|
| **VVR** | Versicherungsvermittler — brokers, agents, tied agents, advisers | § 34d GewO |
| **FAV** | Finanzanlagenvermittler & Honorar-Finanzanlagenberater | §§ 34f / 34h GewO |
| **WIK** | Immobiliardarlehensvermittler — mortgage-credit intermediaries | § 34i GewO |
| **DVR** | Darlehensvermittler — loan intermediaries | § 34c GewO |

A single lookup for `Debeka` comes back with the insurance record **and** the financial-investment
record together, because the register answers all four sections in one call.

***

### ⌨️ Input

```json
{
  "lookups": [
    "Debeka",
    "Schmidt, Andreas",
    "D-F-141-WS31-91"
  ],
  "ort": "",
  "registers": ["VVR", "FAV", "WIK", "DVR"],
  "entityType": "ALL",
  "resolvePreviews": true,
  "maxPreviewsPerLookup": 5,
  "autoNarrow": true,
  "maxLookups": 25,
  "proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "DE" }
}
```

**Each line of `lookups` is auto-detected:**

- `D-F-141-WS31-91` → a **registration number**. Spaces and missing dashes are repaired
  (`DF141WS3191` works). Cheapest and most exact: one request, one full record.
- `Schmidt, Andreas` → a **person**. The comma matters — the register refuses a surname on its
  own, so a line without a comma is read as a company name rather than guessed at.
- `Debeka` → a **company name**.

For anything more precise — a person narrowed to a town, a street-level search, or your own CRM id
echoed back in the output — use `advancedLookups`:

```json
{
  "advancedLookups": [
    { "name": "Schmidt", "vorname": "Andreas", "ort": "Berlin", "label": "crm-4711" },
    { "unternehmensname": "Debeka", "plz": "56073" }
  ]
}
```

***

### 🧩 How this actor beats `TOO_MANY_HITS`

Narrowing is the whole mechanic, and it is measured, not theoretical:

| Query | Result |
|---|---|
| `name=Schmidt&vorname=Andreas` | VVR → **`TOO_MANY_HITS`, zero rows** |
| `name=Schmidt&vorname=Andreas&ort=Berlin` | VVR → **one complete record, `D-E93Q-TDOYE-63`** |

With **`autoNarrow`** on (the default), a refused sub-register query is retried against your own
`narrowCities` list first, then against any town the **other** sub-registers already named for that
same identity in the same response — so a refused insurance query gets answered from a town the
financial-investment register happened to reveal.

**And then it tells you the truth about it.** A narrowed answer covers *one town only*, so every
row from it is marked `lookupComplete: false`, the run is marked `runComplete: false`, and a free
row spells out which town was searched and that intermediaries of that name elsewhere in Germany
are **not** in your output.

***

### 💸 Pricing — and what is deliberately free

| Event | Price | Fires |
|---|---|---|
| **Per full register record** | **$0.004** | Once for each complete statutory record delivered — whether it came back directly or was upgraded from a match |
| **Per match without the full record** | **$0.001** | Once per bare match (number + name + town) delivered, and only when `resolvePreviews` is off or the register would not return the full record |

**Never charged, always delivered:**

- `NO_HITS` — the register holds no such entry
- `TOO_MANY_HITS` — the register refused the query, with its own narrowing instruction quoted
- `NARROWED` — the refused query was answered for one town only, and which town
- `TRUNCATED` — your `maxPreviewsPerLookup` cap dropped matches, **listing the registration
  numbers it dropped** so you can feed them back in
- `INVALID_INPUT` — the lookup could not be searched, and why
- `CHARGE_CAP` — your `maxTotalChargeUsd` stopped rows being delivered, saying how many were
  fetched-but-not-delivered and listing their registration numbers
- rows your `entityType` or `registers` filter removed
- duplicate registration numbers

A match that gets upgraded to a full record is billed **once**, as a record — never also as a
match. Both of Apify's auto-events (`apify-default-dataset-item` and `apify-actor-start`) are
removed, so your bill shows only the two events above.

#### If you set a maximum charge on the run

Apify's per-run **`maxTotalChargeUsd`** is honoured by reserving **money, not rows**, before every
push, so nothing is ever delivered unpaid or charged undelivered. When the cap bites mid-answer the
actor says so instead of looking finished:

- the surviving rows of the truncated batch are marked **`lookupComplete: false`** — the rest of
  that lookup's answer was fetched and thrown away;
- `runComplete` goes **false**, and `RUN_SUMMARY` carries `chargeCapReached` and
  `rowsDroppedByChargeCap`;
- a free **`CHARGE_CAP`** row lists the registration numbers that were dropped;
- the run's **status message names `maxTotalChargeUsd`** as the reason it is short.

***

### 🔐 How the register's gate is handled

vermittlerregister.info protects its search routes with **FriendlyCaptcha** — and it is important
to be precise about what that is. It is **not** an interactive human challenge: there is no image
grid, no checkbox, no "select all the traffic lights". The page downloads a puzzle and its own
background workers grind a **BLAKE2b proof-of-work**; the visitor just watches a progress bar. This
actor performs exactly that computation, reading the site's sitekey from the page at the start of
every run and solving the puzzle against the same public issuer the page uses.

**The register also rate-limits by IP, and raises the puzzle's difficulty against an egress that
pushes too hard. This actor honours that instead of evading it:**

- **one request in flight at a time**, never parallel;
- a **hard floor of 1.2 s** between requests (`minRequestIntervalMs`);
- **one sticky proxy session for the whole run** — there is deliberately **no IP rotation**
  anywhere in this actor;
- a puzzle is solved **only when the register asks for one** (its own 403), never speculatively;
- if the difficulty climbs 6 points above where the run started, the actor **slows to one request
  every 5 s**;
- above `maxCaptchaDifficulty` (**185** by default) it **stops the run cleanly** and says so,
  rather than grinding through the penalty — and it reads the difficulty out of the puzzle
  **before** solving it, so refusing costs you no CPU at all.

`proxyConfiguration` is exposed so you can bring your own German egress if you need more
throughput than one polite session gives you.

#### Measured proof-of-work cost (Apify RESIDENTIAL, country DE, 2026-09-07/08)

| Difficulty seen | Work per solve | Solve time, one desktop core |
|---|---|---|
| 139 | ~8.2 M hashes | 2.1 s |
| 155 | ~30.6 M hashes | 8.2 s |
| 166 | ~84.7 M hashes | 23–25 s |
| 170 | ~112 M hashes | 40 s |
| 194 *(Apify's own datacenter egress — i.e. no proxy)* | ~839 M hashes | **296.9 s** |
| 195 *(a heavily hammered IP, not a normal egress)* | ~1,050 M hashes | ~200 s |

> **Use a German residential proxy. It is not decoration.** Measured 2026-09-08: the same
> single-name lookup that takes **45 s** through Apify RESIDENTIAL/DE took **592 s** with no proxy,
> because the register served difficulty 194 and each of the two solves cost ~290 s of a full vCPU
> — $0.20 of compute for 8 rows. The default `maxCaptchaDifficulty` of 185 now refuses that egress
> before a single hash is computed.

One solve is worth far more than one request: on a pinned session on 2026-09-08 a single solve
carried **20 consecutive register requests** before the register asked for another.

***

### 📊 Measured performance

#### On Apify, at the default 4096 MB, through Apify RESIDENTIAL country-DE (2026-09-08)

The documented demo input — `lookups: ["Debeka", "Gothaer", "HanseMerkur"]` — run five times.
Platform cost is the **settled** `usageTotalUsd`, read after the counters stopped moving (it lags
20–60 s and reads low if you take it at exit):

| Run | Result | Wall clock | Rows | Charged | Platform cost |
|---|---|---|---|---|---|
| 1 (cold, empty cache) | SUCCEEDED | **51.4 s** | 15 full records + 1 free `NO_HITS` row | 15 × `register-record` | $0.0195 |
| 2 | SUCCEEDED | 35.5 s | 16 | 15 | $0.0129 |
| 3 | SUCCEEDED | 38.6 s | 16 | 15 | $0.0139 |
| 4 — **empty input `{}`** | SUCCEEDED | 39.3 s | 16 | 15 | $0.0141 |
| 5 — **empty input `{}`** | SUCCEEDED | 40.2 s | 16 | 15 | $0.0144 |

- **Delivered == billed, exactly.** 15 full records delivered, `chargedEventCounts` = 15
  `register-record`. The `NO_HITS` row was delivered free, as designed.
- The cold run spent **14 register requests and 1 proof-of-work solve**. Runs 2-5 spent **3**
  requests — the three searches, which are never cached — because the eleven record fetches came
  back from the 24-hour record cache. That is a **60-70 % cut in wall clock** on a repeated
  watchlist, with identical output.
- An **empty input** does not fail: it falls back to the documented demo and returns rows.

A further **19 platform runs** were made on 2026-09-08 to check every input against a paired
control (filters, caps, narrowing, dedupe, cross-references, the charge cap, a zero-row run).
All 19 **SUCCEEDED**, none was slower than **75 s**, and settled platform cost ran
**$0.011 – $0.026** per run.

#### How fast this actually goes

Wall clock is dominated by **one proof-of-work solve**, and that is a near-fixed cost per run:

| What | Measured, 2026-09-08, Apify RESIDENTIAL/DE |
|---|---|
| Difficulty the register applied | **166** on every run of the day (170 twice) |
| One solve | 22 – 34 s of one vCPU |
| Requests one solve carried | **up to 8 gated requests with zero 403s** (14 requests, 6 of them the captcha-free `querverweis` route) |
| Gap between requests | the 1.2 s floor + ~0.4 s of network |
| A three-name demo run | 30 – 52 s end to end |

So on one polite session at difficulty 166 the sustained ceiling is roughly **8 register requests
per solve-cycle of ~40 s ≈ 12 requests/minute ≈ 700 per hour**, and a lookup costs one search plus
one request per full record. On a fresh, unpenalised egress (difficulty 139, a 2.1 s solve) the same
cycle takes ~12 s, so several thousand requests an hour is arithmetically possible — but that is the
register's headroom to give, not ours to take, and the actor will not chase it by rotating IPs.
Split large watchlists across runs, or bring your own German egress with `proxyConfiguration`.

#### Repeat runs and the record cache

`cacheMaxAgeHours` (24 by default) keeps full records fetched by registration number in a named
key-value store, so a daily run over the same watchlist re-asks the register only for the searches.
**Searches are never cached**, so every run still reflects today's register.

One deliberate exception: the register serves a record's `querverweis` cross-references **only to a
session that fetched that record itself**. Measured on a warm cache, 5 of 6 cross-reference calls
came back HTTP 400 for exactly that reason. So a run with **`includeCrossReferences` on does not
read the cache** — it re-fetches each record and the links come back populated (verified: 6 of 6).
Rows that do reach you from the cache with cross-references requested say so in
`crossReferencesError` rather than reporting a bare HTTP error.

#### Local live run, 5 lookups, no cache

`Debeka`, `Gothaer`, `HanseMerkur`, `Schmidt, Andreas`, `ERGO Beratung` with
`maxPreviewsPerLookup: 3`, at difficulty 166:

- **27 rows: 24 full register records + 2 free `TRUNCATED` rows + 1 free `NO_HITS` row**
- 24 register requests · **2 proof-of-work solves** (47.2 s of CPU) · 90 s wall clock
- an even split across the three live sub-registers (9 VVR / 9 FAV / 9 WIK; the fourth, DVR, did
  not appear)

#### Local live run, filters and narrowing

`Mueller, Peter`, `Gothaer`, a deliberately fake company; `entityType: "COMPANY"`,
`resolvePreviews: false`, `narrowCities: ["Hamburg"]`:

- 2 full records + 8 bare matches + 3 free `NO_HITS` rows
- **2 `TOO_MANY_HITS` refusals rescued by narrowing** to Hamburg — and both flagged
  `lookupComplete: false` with a free `NARROWED` row naming the town
- **4 rows removed by the entityType filter, none of them billed**
- 7 register requests · 2 solves · 65 s wall clock

#### Field fill, across the 15 full records of the cold platform run

| Field | Filled |
|---|---|
| registrierungsnummer · register · taetigkeitsart · taetigkeitsartText · gewoParagraph · entityType · displayName · firma · strasse · plz · ort · registerbehoerde name/street/no./postcode/town · letztesUpdate · sourceUrl | **100 %** |
| vertretungsberechtigte (named directors) | 87 % |
| hausnummer · querverweisAnfrage | 80 % |
| registerbehoerdeBundesland | 73 % |
| registerbehoerdeLand | 53 % |
| ortsteil · erlaubnisbehoerde (name + full address + under-review flag) | 47 % |
| erlaubnisumfang — all three permit-scope booleans | 33 % *(the register publishes these on FAV/HOF records only)* |
| unmittelbarMitwirkende | 20 % |
| name / vorname *(sole traders only — 2 of these 15 entries)* | 13 % |
| anrede · auslandstaetigkeiten | 7 % |

Fields at 0 % in that sample — `trittAufAls`, `gebundenerVermittler`, `namenszusatz`,
`leitendeAngestellte`, `personenhandelsgesellschaften`, `weitereRegistrierungen`, the authority
phone and email — are ones the register simply leaves empty on most entries. They are parsed and
delivered whenever the register fills them.

### 👤 Personal data — stated plainly

**The Vermittlerregister is overwhelmingly made up of named natural persons.** Most entries are
sole-trader intermediaries, published by name under a statutory transparency mandate (§§ 34d / 34f /
34h / 34i GewO) so that consumers can check whether the person advising them holds a permit. Company
entries additionally name their directors, senior employees and directly involved staff.

Two consequences you should design around:

1. **`entityType` is a first-class input.** Set it to `COMPANY` to keep only registered firms, or
   `PERSON` to keep only individuals. The filter runs **before** anything is billed, and the actor
   will not even spend a request resolving a match it can already see the filter will discard.
2. **The register publishes no contact details for intermediaries.** No email, no phone — the only
   phone and email fields in the data belong to the supervising chamber, and in every record
   captured during development they came back empty. That is exactly why this is a **verification
   and enrichment** product, not a contactable lead list. If someone sells you the German register
   as an email list, they did not get the emails from the register.

***

### ⚖️ Source, robots.txt and fair use

Data source: **https://www.vermittlerregister.info** — the Vermittlerregister of the DIHK
(Deutscher Industrie- und Handelskammertag), the statutory public register under § 11a GewO.

`robots.txt` as served on 2026-09-07, quoted in full:

```
User-agent: *
Disallow: /inhalt
Disallow: /recherche?
Disallow: /en/recherche?
Disallow: /bekanntmachungen/versicherungsvermittler-nichtanonymisierte
Disallow: /bekanntmachungen/immobiliardarlehensvermittler-nichtanonymisierte
Disallow: /bekanntmachungen/versicherungsvermittler-anonymisierte
Disallow: /bekanntmachungen/immobiliardarlehensvermittler-anonymisierte
Disallow: /en/notices/insurance-brokers-non-anonymised
Disallow: /en/notices/mortgage-brokers-non-anonymised
Disallow: /en/notices/insurance-brokers-anonymised
Disallow: /en/notices/mortgage-brokers-anonymised

Sitemap: https://www.vermittlerregister.info/sitemap.xml
```

The full file is also written into every run's `RUN_SUMMARY` record, so you always have the version
that was live when your data was collected. Read it and make your own call. Note that the register's
data routes under `/suche/` are not listed in it, and that the disallowed `/bekanntmachungen/*`
notice lists are **not** touched by this actor.

This is public register data published for public verification. You are responsible for how you use
it — in particular for GDPR compliance when the entry is a natural person, and for respecting the
register's own rate limits, which is why this actor is deliberately slow and never rotates IPs.
Not affiliated with, endorsed by, or connected to the DIHK or any IHK.

***

### ❓ FAQ

**Can I export all ~190,000 registered intermediaries?**
No, and neither can anyone else, including through the register's own website. There is no listing
endpoint, no pagination parameter, and the register refuses broad queries by design. Anything
promising you a full dump of this register is not getting it from this register.

**What exactly happens when a query is "too broad"?**
The register returns HTTP 200 with `status: "TOO_MANY_HITS"` and `data: null` for that sub-register.
This actor writes a **free** row carrying that status, the register's own narrowing instruction, and
a list of the towns it tried on your behalf. You are never charged for a refusal.

**How do I search for a person?**
Write `Surname, Firstname` — the comma is what tells the actor it is a person, and the register
needs both halves (at least two characters each). Without a comma the line is treated as a company
name. `advancedLookups` gives you explicit `name` / `vorname` fields if you prefer.

**Why did my common surname return nothing?**
Because the register refused it. Add an `ort` or `plz`, put likely towns in `narrowCities`, or look
the person up by registration number. See the `TOO_MANY_HITS` row in your dataset for the specifics.

**Does it get email addresses or phone numbers?**
No. The register does not publish them. See *Personal data*, above.

**What is `taetigkeitsart` and why does it matter?**
It is the code for the exact statutory permission, and it is the single most valuable field here.
A `3` on a VVR record means *gebundener Versicherungsvertreter* under § 34d Abs. 7 GewO — a tied
agent, not an independent broker — which is precisely the distinction a pool operator, an MGA or a
compliance team needs and which no Handelsregister extract, Maps listing or CRM enrichment gives
you. The actor decodes it into `taetigkeitsartText` and `gewoParagraph` using the register's own
wording.

**Why is it slow?**
Because it is polite on purpose. The register applies a per-IP proof-of-work and a per-IP rate
limit; the fastest way to make a run *slower* is to push harder, which makes the register raise the
difficulty. One sticky session, one request at a time, 1.2 s apart. Use `proxyConfiguration` to
bring your own egress if you need more throughput.

**Can I run it daily over the same watchlist?**
Yes — that is what `cacheMaxAgeHours` is for. Full records fetched by registration number are kept
in a named key-value store and reused for 24 hours by default, so a daily run does not re-ask the
register for records that have not changed. Searches are **never** cached, so every run still
reflects today's register. Cached rows are marked `fromCache: true`. The one exception is
`includeCrossReferences` — see *Repeat runs and the record cache*.

**How do I know whether my run is complete?**
Every row carries `lookupComplete`, and the `RUN_SUMMARY` record carries `runComplete` plus the
exact counts: refusals, narrowed answers, matches dropped by your cap, rows removed by your filters,
duplicates suppressed, requests spent and puzzles solved. If anything truncated your run, the run's
status message names the cap that did it.

**What happens if the register changes its gate?**
The sitekey and the search base URL are read from the live page at the start of every run, never
hard-coded, so a rotated key is a non-event. If the register ever switches to reCAPTCHA — an
interactive human challenge — this actor stops and tells you that in plain language rather than
returning half-data. It does not attempt human-verification challenges.

**Is a run that finds nothing a failed run?**
No. It exits cleanly with a status message naming the real cause — your cap, your budget, the
register's refusal or a genuine absence — and the free status rows in the dataset explain each
lookup individually.

# Actor input Schema

## `lookups` (type: `array`):

The identities to resolve, one per line. Each line is auto-detected:

• **A registration number** — `D-F-141-WS31-91`, `D-W-139-JLDM-77` or `D-URTZ-05QX4-94` (spaces and missing dashes are fixed for you). This is the cheapest and most exact lookup: one request, one full record.
• **A person, as `Surname, Firstname`** — `Schmidt, Andreas`. The comma matters: the register refuses a surname on its own, so a line WITHOUT a comma is read as a company name rather than guessed at.
• **Anything else** — a company name, e.g. `Debeka` or `Allianz Beratungs`.

Each lookup is searched across all three sub-registers in ONE request, so `Debeka` can return an insurance record and a financial-investment record together.

**This is a batch lookup tool, not a bulk exporter.** The register has no listing route and no page parameter, and it refuses any query that is too broad with `TOO_MANY_HITS` — see the FAQ in the README.

## `advancedLookups` (type: `array`):

Full control over each lookup, for when auto-detection is not enough — a person whose name contains a comma, a company narrowed to one town, or a street-level search. Every key is optional except that you need `unternehmensname`, OR both `name` and `vorname`, OR `registrierungsnummer`.

```json
[
  { "name": "Schmidt", "vorname": "Andreas", "ort": "Berlin" },
  { "unternehmensname": "Debeka", "plz": "56073" },
  { "registrierungsnummer": "D-F-141-WS31-91" },
  { "unternehmensname": "M\u00fcller", "strasse": "Hauptstr", "hausnummer": "1", "label": "my CRM id 4711" }
]
```

`label` is echoed back in `queryInput` so you can join the output to your own records. These run BEFORE the plain `lookups` list. Accepted keys: `unternehmensname`, `name`, `vorname`, `registrierungsnummer`, `ort`, `plz`, `strasse`, `hausnummer`, `label` — the exact parameter set the register's own search form uses.

## `ort` (type: `string`):

Applied to every lookup that does not carry its own `ort`. This is the single most useful knob on this actor: a common surname comes back `TOO_MANY_HITS` with no rows, and adding a town collapses it to one full record (measured: `Schmidt, Andreas` refused, `Schmidt, Andreas` + Berlin returned the complete record D-E93Q-TDOYE-63).

## `plz` (type: `string`):

Applied to every lookup that does not carry its own `plz`. Use it instead of `ort` when you know the postcode — it is stricter and more reliable than a town name.

## `strasse` (type: `string`):

Street name without the house number, applied to every lookup that does not carry its own `strasse`. Rarely needed; useful for a very common company name at a known address.

## `hausnummer` (type: `string`):

House number, applied to every lookup that does not carry its own `hausnummer`. Only meaningful together with `strasse`.

## `autoNarrow` (type: `boolean`):

When one sub-register refuses a lookup as too broad, retry it narrowed to a town — first the towns from **Narrowing cities** below, then any town the OTHER sub-registers already named for that same identity in the same response. This is how a refused insurance query gets answered from a hit that the financial-investment register happened to return. Each retry costs one extra register request. Rows found this way carry `narrowedBy`, so the narrowing is never hidden from you.

## `narrowCities` (type: `array`):

Towns to try first when auto-narrowing a refused query — e.g. the cities your target list is drawn from. Tried before the towns discovered from the register's own response.

## `maxNarrowAttempts` (type: `integer`):

How many towns to try before giving up on one refused sub-register query and writing an honest, unbilled TOO\_MANY\_HITS row instead. Each attempt is one register request. Set 0 to never retry.

## `registers` (type: `array`):

Which of the register's four sections to keep. All four are searched in the same single request, so excluding one does not save a request — but it does stop this actor spending extra requests resolving that section's matches, and rows it removes are never billed.

• **VVR** — Versicherungsvermittler (insurance brokers, agents, tied agents, advisers; § 34d GewO)
• **FAV** — Finanzanlagenvermittler and Honorar-Finanzanlagenberater (§§ 34f / 34h GewO)
• **WIK** — Immobiliardarlehensvermittler (mortgage-credit intermediaries; § 34i GewO)
• **DVR** — Darlehensvermittler (loan intermediaries). Present in the register's own client contract; we have not seen it in a live response, and pass through whatever the register returns.

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

**Read this before you run a person-level export.** The Vermittlerregister is overwhelmingly made up of NAMED NATURAL PERSONS — sole-trader intermediaries published under a statutory transparency mandate (§§ 34d/34f/34h/34i GewO). Choose deliberately:

• **ALL** — every entry the register returns.
• **COMPANY** — only entries held by a firm (`firma`). The named directors on those records still appear in `vertretungsberechtigte`.
• **PERSON** — only entries held by a named individual.

Applied before billing: rows this filter removes are not delivered and not charged, and when the filter is set this actor will not even spend a request resolving a match it can already see will be dropped.

## `resolvePreviews` (type: `boolean`):

When a lookup matches more than one entry, the register returns a short preview list (registration number, name, town) instead of full records. With this on, each preview is fetched as a complete record — permit type, permit scope, address, named directors, supervising authority — at the cost of one extra register request per match.

Turn it off to pay $0.001 per bare match instead of $0.004 per full record, e.g. when you only want to know WHETHER someone is registered.

## `maxPreviewsPerLookup` (type: `integer`):

A ceiling on how many matches of ONE lookup in ONE sub-register are turned into rows. It bounds both cost and runtime on a name that matches many people. When it truncates, the run writes a free `TRUNCATED` row listing the registration numbers it dropped, and every row of that lookup is marked `lookupComplete: false`. Set 0 for no ceiling.

## `includeCrossReferences` (type: `boolean`):

For each full record, follow the register's own `querverweis` link, which reports the same intermediary's entries in the other sub-registers. It costs one extra request but NO proof-of-work budget. Off by default because most records answer with an empty list.

Note: the register only serves a record's cross-references to a session that fetched the record itself, so turning this on makes the run READ nothing from the 24-hour record cache (it still writes to it). Measured: with the cache in play, 5 of 6 cross-reference calls came back HTTP 400.

## `emitStatusRows` (type: `boolean`):

Write an unbilled row for every lookup that produced no record — `NO_HITS`, `TOO_MANY_HITS`, `INVALID_INPUT`, `TRUNCATED` — carrying the reason in plain language plus the register's own narrowing instruction. These rows are NEVER charged; they exist so you can audit a run and see exactly which of your inputs failed and why. Turn them off only if your pipeline cannot tolerate mixed row types.

## `deduplicate` (type: `boolean`):

Deliver each `register + registration number` at most once per run, so overlapping lookups (a company name and its registration number, say) do not bill twice for identical data.

## `maxLookups` (type: `integer`):

How many lookups from your list this run will attempt. It is a hard stop on runtime and cost, and it is applied to the FRONT of the list. If it truncates your input the run says so in its status message and marks itself incomplete. Set 0 for no limit — but read the throughput note in the README first: this actor deliberately honours the register's rate limit and runs at roughly 25-30 lookups a minute.

## `maxRequests` (type: `integer`):

A second, lower-level ceiling: the total number of HTTP requests this run may send to vermittlerregister.info, counting searches, record fetches and narrowing retries. Useful when you want a hard bound regardless of how many matches your lookups turn out to have. Set 0 for no limit.

## `minRequestIntervalMs` (type: `integer`):

The floor between two register requests, in milliseconds. 1200 ms is what was measured as sustainable on a fresh German residential egress. Lowering it does not make a run faster in practice — the register answers by raising its proof-of-work difficulty for your IP, which costs far more time than it saves.

## `maxCaptchaDifficulty` (type: `integer`):

The register's search gate is an automatic BLAKE2b proof-of-work — the same computation every visitor's browser runs, with no human interaction — and its difficulty is raised per IP against egresses that push hard. This actor reads the difficulty out of the puzzle BEFORE solving it, so this limit costs you nothing to enforce.

Measured 2026-09-08 on one Apify vCPU: Apify RESIDENTIAL country-DE was served 139–170 (2–40 s per solve). Apify's own datacenter egress, with no proxy, was served **194** — one solve is ~839 M hashes and took **296.9 s**, and a run there burned $0.20 of compute for 8 rows.

Above 185, or 6 points above where this run started, the actor slows to one request every 5 s. Above THIS number it stops the run cleanly, before spending the CPU, and tells you why — rather than grinding through the penalty or switching IPs to escape it. Raise it only if you are willing to pay for very long solves.

## `cacheMaxAgeHours` (type: `integer`):

Full records fetched by registration number are kept in a named key-value store and reused for this many hours, so a daily run over the same watchlist does not re-ask the register for records that have not changed. The register republishes once a day (see `letztesUpdate` on every row), so 24 hours is the natural setting. Cached rows are marked `fromCache: true` with the timestamp they were fetched.

Searches are NEVER cached — every lookup still queries the register live, so a run always reflects today's register. Set 0 to disable the cache entirely.

The cache is not READ on a run with `includeCrossReferences` on — see that field.

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

The register applies its proof-of-work gate and its rate limit per egress IP, so a German residential proxy is strongly recommended — that is what the measurements in the README were taken through. Bring your own egress here if you need more throughput than one polite session gives you.

## Actor input object example

```json
{
  "lookups": [
    "Debeka",
    "Gothaer",
    "HanseMerkur"
  ],
  "advancedLookups": [],
  "ort": "",
  "plz": "",
  "strasse": "",
  "hausnummer": "",
  "autoNarrow": true,
  "narrowCities": [],
  "maxNarrowAttempts": 3,
  "registers": [
    "VVR",
    "FAV",
    "WIK",
    "DVR"
  ],
  "entityType": "ALL",
  "resolvePreviews": true,
  "maxPreviewsPerLookup": 5,
  "includeCrossReferences": false,
  "emitStatusRows": true,
  "deduplicate": true,
  "maxLookups": 25,
  "maxRequests": 0,
  "minRequestIntervalMs": 1200,
  "maxCaptchaDifficulty": 185,
  "cacheMaxAgeHours": 24,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "DE"
  }
}
```

# Actor output Schema

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

Registration number, permit type decoded to its GewO paragraph, permit scope, entity type, company or personal name, business address, the named authorised representatives and senior staff, the licensing authority and the supervising IHK, and the date the register itself published the data.

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

Whether the run was complete, what each lookup returned, how many register requests and proof-of-work solves it cost, the proof-of-work difficulty range the register applied, and robots.txt as served.

# 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 = {
    "lookups": [
        "Debeka",
        "Gothaer",
        "HanseMerkur"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "DE"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/vermittlerregister-de-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 = {
    "lookups": [
        "Debeka",
        "Gothaer",
        "HanseMerkur",
    ],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "DE",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/vermittlerregister-de-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 '{
  "lookups": [
    "Debeka",
    "Gothaer",
    "HanseMerkur"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "DE"
  }
}' |
apify call scrapersdelight/vermittlerregister-de-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapersdelight/vermittlerregister-de-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/t2ivB31cYTyTwrQuy/builds/pASFK1CemYoi5lNA2/openapi.json
