Marktstammdatenregister Scraper — MaStR Operator Leads avatar

Marktstammdatenregister Scraper — MaStR Operator Leads

Pricing

from $1.80 / 1,000 per row returneds

Go to Apify Store
Marktstammdatenregister Scraper — MaStR Operator Leads

Marktstammdatenregister Scraper — MaStR Operator Leads

Scrape Germany's statutory energy register (Marktstammdatenregister): every solar, wind, battery, biomass and hydro unit with capacity, registration date, town, status and the operator company — optionally enriched with the operator's address, Handelsregister number and VAT ID.

Pricing

from $1.80 / 1,000 per row returneds

Rating

0.0

(0)

Developer

Scrapers Delight

Scrapers Delight

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

Share

⚡ Marktstammdatenregister Scraper — German energy assets and the companies that own them

Every solar park, wind farm, battery, biogas plant and gas turbine in Germany has to be registered with the Bundesnetzagentur in the Marktstammdatenregister (MaStR) — by law. That makes it one of the very few places on the open web where a whole country's capital spending is listed, dated, sized, geolocated and attributed to a named company.

This actor turns that register into a lead list. It pulls the installations you filter for and — optionally — looks each operator up in the register's own market-actor directory to add its street address, Handelsregister court and number, VAT ID and SME flag.

The trigger that makes it a lead, not a database dump: a unit's registration date. A company that registered a 6 MW wind turbine last week has just committed capital and is now buying O&M, metering, direct marketing, grid services, insurance and financing. At or above 100 kW, 1,449 new units were registered in the last 30 days (measured 2026-09-06 on build 0.1.13, run kxGaTaHQjsA6HjnEh).


📊 What you get, per row

Every unit row carries the same 48 columns (50 if you switch includeRawJson on; operator roll-up mode has its own shape — see the roll-up FAQ below). 26 of them are filled by the base scrape; the other 22 — the whole operator_* enrichment block — are null unless you switch enrichOperator on, which is OFF by default. Both halves are spelled out below.

The base scrape — what a default run returns

Measured on a real 2,000-row platform run (≥ 100 kW, newest first, 2026-09-06, build 0.1.13, run 16gi9uVtQ2W7skxT4 — 5,000 rows fetched at page size 5,000, 2,000 delivered, all unique):

FieldFillWhat it is
mastr_number100%The official Bundesnetzagentur unit id (SEE902744940317) — the stable dedupe key
unit_name100%The display name the operator gave it ("Gasturbinen Kraftwerk Leipheim 20", "Solarpark Nord")
gross_capacity_kw / net_capacity_kw100%Bruttoleistung / Nettonennleistung, in kW
energy_carrier100%Wind · Solar · Battery storage · Biomass · Hydro · Natural gas · Hydrogen · 13 more
operating_status (+ id)100%In Betrieb · In Planung (the project pipeline) · Vorübergehend / Endgültig stillgelegt (the repowering lead)
registered_at100%🔔 The buying trigger — when the unit entered the register
last_updated_at100%Catches changes to existing assets that a registration filter never sees
unit_type_id100%The register's internal unit-type code
unit_detail_url100%Deep link to the unit's public page on the register
operator_name100%🏢 The company that owns it
operator_mastr_number100%Its ABR… registry id — pull its whole portfolio with one input field
operator_profile_url100%Deep link to the operator's public register page
operator_person_type / is_anonymised / operator_enriched100%Always present flags
source_filter / scraped_at100%Which request stream produced the row, and when
town / postcode99.5%Where the installation sits
location_id61.0%Grid-connection location id — groups units on one connection point
commissioned_at60.9%When the asset actually went live (a planned unit has none)
solar_module_count49.4%PV only — a proxy for physical array size
cadastral_parcel39.8%Flurstück, the land parcel
is_pilot_wind1.2%Populated on wind rows only — 24 of the 2,000 rows carried it, all false (run 16gi9uVtQ2W7skxT4)

🏢 With operator enrichment switched on (enrichOperator: true)

These 22 columns are null on every row of a default run — verified today: 0 of the 2,000 rows of run 16gi9uVtQ2W7skxT4 carried any of them. Turn enrichOperator on and they fill like this, measured on a real 300-row platform run covering 124 distinct operator companies (2026-09-06, build 0.1.13, run RFDaZWjI6gs9oYMQv):

FieldFillWhat it is
operator_legal_name100%The registered legal name
operator_street100%Head-office street
operator_house_number98.7%
operator_postcode / operator_town100%Often a different town from the asset — this is where the decision-maker sits
operator_state / operator_country100%
operator_market_function / operator_activity_status100%Market role, and whether the company is still active (Aktiv on 300/300)
operator_registered_at / operator_last_updated_at100%How long the company has been in the register
operator_industry_group_id / operator_region_id100%The register's industry-group and NUTS-II region codes
operator_is_sme95.7%Mittelstand vs enterprise — 89 of the 300 rows were SMEs
operator_register_court92.3%Handelsregister court (e.g. "Essen")
operator_register_number92.0%HRA / HRB number (e.g. "HRB24353") — the key that joins to Handelsregister, Northdata, Creditreform
operator_vat_id79.3%USt-IdNr. (e.g. "DE114216469")
operator_activity_start69.3%When the company started trading in this market role
operator_acer_code51.0%EU ACER market-participant code
operator_market_roles · operator_bnetza_number · operator_activity_end0%The register returns these three empty — see the FAQ. Kept as columns so the schema matches the source.

A real enriched row, exactly as returned by run RFDaZWjI6gs9oYMQv on 2026-09-06:

{
"unit_name": "Gasturbinenkraftwerk Westfalen GT3",
"gross_capacity_kw": 57000, "energy_carrier": "Erdgas",
"registered_at": "2026-09-05T00:00:00.000Z",
"operator_legal_name": "RWE Generation SE",
"operator_street": "RWE-Platz", "operator_house_number": "3",
"operator_postcode": "45141", "operator_town": "Essen",
"operator_register_court": "Essen", "operator_register_number": "HRB24353",
"operator_vat_id": "DE114216469", "operator_is_sme": false,
"operator_acer_code": "A00027354.DE"
}

🎯 Who buys this

  • Energy-services sales — O&M contractors, direct marketers (Direktvermarkter), metering operators, PPA desks, grid-connection consultants. New registration = a fresh contract to win.
  • EPCs, installers and repowering developersIn Planung is a pipeline of assets not yet built; Endgültig stillgelegt is a pipeline of sites ready to be repowered.
  • Component and inverter manufacturers — segment by carrier, capacity band and module count.
  • Insurance, finance and leasing — capital assets with a dated owner and a Handelsregister number.
  • M&A, market research and investors — operator roll-up mode gives portfolio size and total MW per company. The totals are the company's whole matching portfolio, not a sum of whatever the crawl happened to fetch and not a sum of whatever this particular run delivered: once maxItems has picked which companies you get, each of those companies' full set of matching units is re-read from the register and aggregated before dedupeAcrossRuns / excludeAnonymised decide which units get listed. Units those two switches hold back are still counted in unit_count and the kW totals — units_withheld and units_withheld_reason say how many and why (see Operator roll-up below).
  • Data teams — the Handelsregister number and VAT ID join cleanly to Handelsregister, Northdata, Creditreform and Bundesanzeiger records.

🔎 What you can filter on

25 of the register's 27 filter names are wired — everything except the two offshore area-development-plan codes (Gebiet nach dem Flächenentwicklungsplan des BSH (Nordsee) and (Ostsee)), which have no input field:

  • Size — min/max gross capacity, min/max net capacity, min/max solar-module count
  • Technology — 20 energy carriers (multi-select)
  • Status — In Betrieb · In Planung · Vorübergehend / Endgültig stillgelegt (multi-select)
  • Timing — registered / commissioned / last-updated, each with an on-or-after and an on-or-before bound, plus a registeredLastNDays shorthand for scheduled monitors
  • Place — 16 federal states + the offshore Exclusive Economic Zone, 10 countries, town, postcode, district (Landkreis), municipality, municipality key, street, cadastral parcel and district, and a lat/lon bounding box
  • Identity — operator name contains, unit name contains, exact unit SEE… numbers, exact operator ABR… numbers (which returns that company's entire portfolio)
  • Escape hatch — paste a marktstammdatenregister.de grid URL and the actor reuses its filter, sort and page size verbatim

Plus output controls: one row per installation or one row per operator (units nested and the company's whole matching portfolio totalled — the totals are re-read from the register after maxItems has picked the companies and aggregated before dedupeAcrossRuns / excludeAnonymised narrow the nested list, so they are never summed off a truncated crawl), 12 sort fields, four date formats, raw-JSON passthrough, cross-run dedupe, and a hard row cap.


🚀 Example inputs

Every one of these was run on 2026-09-06 against the live actor; the run id and what it actually returned are printed under each.

The monitor — every commercial unit registered in the last week, as a company lead:

{
"minGrossCapacityKw": 100,
"registeredLastNDays": 7,
"enrichOperator": true,
"dedupeAcrossRuns": true,
"maxItems": 1000
}

Pass 1, run dAhoz90Pf2cl6y8Nr: 456 units matched the 7-day window, 413 rows delivered in 75.7s — 43 dropped as anonymised private persons. Billed 413 rows + 192 unique operators. Pass 2 immediately afterwards, run DAu92LgWF5QCubg9s, same input: the same 456 units matched, 413 were skipped as already delivered, 43 as anonymised — 0 rows delivered, 0 rows billed, 0 operator lookups billed, in 6.5s. That is what a scheduled monitor costs on a quiet day. dedupeAcrossRuns remembers those MaStR numbers in a named key-value store, mastr-energy-operator-scraper-seen, on your own account. That is why pass 2 is empty. To start a fresh list — or to demo pass 1 again — delete the seen-mastr-numbers record in that store, or leave dedupeAcrossRuns off.

The project pipeline — utility-scale assets that are planned but not yet built:

{
"operatingStatus": ["31"],
"minGrossCapacityKw": 1000,
"sortBy": "Bruttoleistung",
"sortOrder": "desc",
"maxItems": 500
}

Run p0dKV6TUR28TRVvKF: 500 rows in 10.0s out of 10,242 planned units above 1 MW.

Repowering targets — decommissioned wind in the north:

{
"energyCarriers": ["2497"],
"operatingStatus": ["38"],
"bundeslaender": ["1408", "1411", "1407"],
"enrichOperator": true,
"maxItems": 1000
}

Run MYUeAio54ffqDGqIN: three request streams, 1,307 decommissioned turbines fetched — and 1,176 of them (90.0%) are owned by anonymised private persons, so only 131 rows are company leads (45 unique operators enriched), in 26.4s. Old small wind is mostly privately held; expect that shape here, not 1,000 rows.

One operator's whole portfolio — every unit a company owns:

{ "operatorMastrNumbers": ["ABR939917736286"], "minGrossCapacityKw": 0, "maxItems": 0 }

Run M2qqeh0ZLRccMBwtx: all 15 of that operator's units in 5.9s.

Operator roll-up — one row per company, with MW totals and its units nested:

{
"minGrossCapacityKw": 2000,
"registeredLastNDays": 365,
"outputShape": "operator",
"enrichOperator": true,
"maxItems": 500
}

Run tiAvtCfPjcZc5edNQ: the crawl scanned 2,000 unit rows, found 500 companies, then re-read those 500 companies' full matching portfolios — 1,662 units — and totalled those. 500 rows delivered, 500 unique operators enriched, 288.8s, 81 MB peak, and all 500 rows came back portfolio_complete: true.

That re-read is the difference between a real portfolio and a sampling artefact. The same input on the previous build (0.1.10, run Sgd5qbjHNbgD4iW1U) summed only the units the crawl had fetched: over the 499 companies both runs returned, it reported 1,088 units and 16,402,496 kW where the true matching portfolios hold 1,661 units and 21,741,394 kW — 100 of those 499 rows were short, RWE Generation SE by 114 → 142 units and Borkum Riffgrund 3 by 2 → 83. Four of the corrected rows were checked straight against the register's own filtered totals and matched exactly (RWE Generation SE 142/4,132,035.4 kW, RWE Wind Onshore & PV 83/618,232.3, Borkum Riffgrund 3 83/958,650.0, EnBW He Dreiht 51/765,000.0).


💵 Pricing

EventPriceWhen it fires
Per row returned$0.0018Once per row written to your dataset
Per operator enriched$0.0018Once per unique operator company looked up — only when enrichment is on
  • You are never billed for a row you did not receive. Rows are written and charged in the same atomic call, so hitting a charge cap stops delivery and billing together. Verified on every run listed on this page: unit-scraped equals the delivered row count exactly (50/50, 300/300, 400/400, 413/413, 500/500, 2000/2000 …), polled past the counter lag until stable.
  • Filtered-out rows are free. Anonymised private persons, duplicates and units already delivered by an earlier run are dropped before anything is charged. Run MYUeAio54ffqDGqIN fetched 1,307 rows, dropped 1,176, and billed 131.
  • A lookup that resolves nothing is not billed. If the market-actor register has no record for an operator, that unit is still delivered — with the operator_* fields null — and no operator-enriched event is charged for it.
  • The enrichment cache is a billing fix, not an optimisation. Measured 2026-09-06 on run RFDaZWjI6gs9oYMQv: 300 unit rows resolved to 124 unique operators, because operators own multiple units. One lookup per row would have overcharged you by 142%.
  • Your charge cap is respected in money, not in row counts, and the rows are reserved first. With enrichment on, a lead costs a row charge plus an operator lookup, so the actor sizes every batch — and every lookup — against the combined price, with the row charges it still owes taken out of the budget before any lookup is allowed to start. That reservation matters most in operator roll-up mode, where nothing is pushed until the crawl is over: measured today, maxTotalChargeUsd: 0.09 (= 50 events) with outputShape: "operator", enrichOperator: true and maxItems: 50 delivers 24 company rows and 24 lookups (run ZygWWPzB0kH2dbFmF), and the same shape at maxItems: 500 under a $0.50 cap delivers 138 rows against 138 lookups (run htYbL07KW9XiHxqQw, $0.4968 of $0.50). On the previous build the identical $0.09 input spent 49 of its 50 events on lookups and delivered one row (0.1.10, run EQvHSBav7nrImw9HD) — that is the bug this reservation fixes.

Sizing, at the corpus measured 2026-09-06 (runs 16gi9uVtQ2W7skxT4 and kxGaTaHQjsA6HjnEh): the whole ≥ 100 kW commercial band is 169,407 units ≈ $304.93; a rolling 30-day monitor at or above 100 kW is 1,449 rows ≈ $2.61/month. Turning enrichment on adds one lookup per unique company, which on that 30-day slice was 124 companies per 300 rows.


⚙️ How it works, and what it does not hide

Plain HTTP against the register's own public JSON endpoint — the same request the site's search grid makes in your browser. No login, no API key, no CAPTCHA, no browser, no anti-bot bypass. 512 MB, and Apify's automatic proxy by default (it resolved to datacenter IPs on every run measured here — run 16gi9uVtQ2W7skxT4 billed 0 GB of residential transfer). Residential is selectable but was never needed.

Reliability, measured on the platform through an Apify proxy, not on a home IP (2026-09-06):

  • 13 platform runs on build 0.1.13, 1,023 operator-lookup calls plus every list page and every portfolio-completion request — zero transport retries and zero failed pages. All 13 finished SUCCEEDED. No HTTP error from the register, no rate limit, no block, no soft-ban. (Earlier builds did hit occasional proxy 595 ECONNRESET / TLS resets; every one recovered on the next attempt, which is what the retry loop is there for.)
  • Pagination is dupe-free: run cg9aFDLyfBe20TKyU walked 23 contiguous 20-row pages with zero retries — 460 fetched, 400 delivered, 400 unique, 0 dupes, in 21.6s. Run 16gi9uVtQ2W7skxT4 pulled a single 5,000-row page and delivered 2,000 / 2,000 unique.
  • No anonymised row and no duplicate ever shipped: across every run cited on this page, is_anonymised: true appears on 0 rows and every mastr_number is unique.
  • Live platform runs on build 0.1.13, all 2026-09-06: 2,000 rows in 42.4s at page size 5,000, peak memory 79 MB (16gi9uVtQ2W7skxT4); 500 planned-unit rows in 10.0s (p0dKV6TUR28TRVvKF); 300 enriched rows in 40.7s (RFDaZWjI6gs9oYMQv); 413 enriched monitor rows in 75.7s (dAhoz90Pf2cl6y8Nr); 500 operator roll-up rows — 2,000 units scanned, 500 lookups, 1,662 units re-read to complete the portfolios — in 288.8s, peak 81 MB (tiAvtCfPjcZc5edNQ); the daily-health input (50 rows) in 5.7s (kxGaTaHQjsA6HjnEh).
  • The full ≥ 100 kW band is 169,407 rows — reachable in ~34 requests at page size 5,000.

Three ways this register silently lies, and what the actor does about them

Every one of these returns HTTP 200 with no error, which is exactly why they are dangerous. All three were re-verified against the live register on 2026-09-06.

  1. An unrecognised filter name is ignored and you get the entire 9.4-million-row corpus. Verified live today: Betriebsstatus~eq~'35' — one missing hyphen — returned all 9,417,097 rows, the whole register. A guessed filter name would therefore bill you for millions of rows that do not match your query. This actor pins the register's 27 legal filter names, refuses to emit anything else, asserts every returned row against the clauses that produced it, and fails the run if the filtered total is not below the unfiltered total.
  2. ~or~ and parentheses are mis-parsed. Verified today: Energieträger~eq~'2497'~or~Energieträger~eq~'2496' returned 43,458 — exactly the Wind-only total, with the Speicher clause (2,777,879 on its own) silently dropped. And (A~or~B)~and~C returned the entire corpus, all 9.4 million rows: the filter was discarded outright. So every multi-select here fans out one request per value and merges on the MaStR number, and a pasted URL containing ~or~ is refused rather than sent.
  3. There is no or . ~gte~, ~lte~, ~ge~, ~le~, ~ne~, ~in~, ~startswith~ and ~endswith~ all return an error envelope — re-confirmed today. Inclusive bounds are reproduced exactly by nudging the strict bound: >99.999999 returns 169,407 units, which is precisely >100 (167,728) plus =100 (1,679), re-measured 2026-09-06. The module-count column is an integer column that rejects decimals (>19999.999999 errors), so it is nudged by a whole 1 — also verified exact today: >19999 (2,062) == >20000 (2,058) + =20000 (4).

Dates are the same story: the register compares only the date part, strictly. Verified today, Letzte Aktualisierung~gt~'2026-09-04' returned 5,441 rows — everything dated 09-05 (5,143) plus what had already been touched on 09-06 — so ~gt~'D' means "on or after D+1", never "on D". The actor shifts each bound by a day so that "on or after 4 September" really means that.


🔒 Privacy — read this

The Bundesnetzagentur anonymises private individuals itself. Where a unit is owned by a natural person rather than a company, the register returns IsAnonymisiert: true, the name literally (natürliche Person), and the market-actor lookup returns that person's address, postcode and VAT fields as null. Verified on a live private-person record.

  • excludeAnonymised defaults to true, so those rows are dropped before delivery and before billing. They are not business leads.
  • Even in the ≥ 100 kW commercial band they are a real share, all measured 2026-09-06 on build 0.1.13: 10.6% of a 5,000-row ≥ 100 kW page (528 of 5,000, run 16gi9uVtQ2W7skxT4), 10.9% of the ≥ 100 kW / last-30-days slice (98 of 900 fetched, run RFDaZWjI6gs9oYMQv) and 12.7% of the smallest slice of it (19 of 150, run kxGaTaHQjsA6HjnEh). In the decommissioned wind slice it is 90.0% (1,176 of 1,307, run MYUeAio54ffqDGqIN) — old small turbines are mostly privately owned, so budget for that if you filter for repowering targets.
  • This actor makes no attempt to re-identify an anonymised operator, and you should not either.
  • The output is company data — legal entities, their registered offices and their public Handelsregister and VAT identifiers. Handling any personal data you do choose to collect (by turning the filter off) is your responsibility under GDPR.

  • robots.txt: https://www.marktstammdatenregister.de/robots.txt answers HTTP 302 and redirects to https://www.marktstammdatenregister.de/MaStR/Assets/static/error.html, which is the site's own German error page — <title>Fehlerseite | MaStR</title>, 6,432 bytes of HTML. There is no robots.txt on this host, so no path is disallowed. Re-checked 2026-09-06.
  • The Marktstammdatenregister is a statutory public register operated by the Bundesnetzagentur, published under §111e/§111f EnWG and the MaStRV. The data this actor reads is the same data the register serves to any visitor of its public search page, through the same request.
  • No login is used, no access control is circumvented, no CAPTCHA is solved, and no request is signed or forged. Requests are polite: 4 parallel requests by default against an endpoint that measured 8-way concurrency clean.
  • You are responsible for how you use the output, including GDPR, UWG and any marketing-consent rules that apply to contacting the companies you find.

❓ FAQ

Which columns are empty unless I turn enrichment on? All 22 of the operator_* enrichment columns: operator_legal_name, operator_street, operator_house_number, operator_postcode, operator_town, operator_state, operator_country, operator_register_court, operator_register_number, operator_vat_id, operator_is_sme, operator_market_function, operator_market_roles, operator_activity_status, operator_activity_start, operator_activity_end, operator_registered_at, operator_last_updated_at, operator_bnetza_number, operator_industry_group_id, operator_region_id, operator_acer_code. enrichOperator is off by default, so a default run ships those 22 columns null on every row and operator_enriched: false — measured 0 of 2,000 rows filled on run 16gi9uVtQ2W7skxT4. The company's name, ABR… id and register profile URL come back either way.

How fresh is the data? Live. Every run queries the register directly — the unfiltered total ticked from 9,417,027 to 9,417,033 during the two minutes between runs mU0BdvYjNmTfEnI7N and ZygWWPzB0kH2dbFmF on 2026-09-06. Units registered today appear today.

How far back does the register go? The MaStR started in 2019. Re-measured 2026-09-06: zero units have a registration date before 2015-01-01, and only 429 before 2019-02-01. Commissioning dates go back much further — the register was retroactively populated with assets built decades earlier.

How big is the corpus? 9,417,097 units in total on 2026-09-06, of which 169,407 are at or above 100 kW (167,728 strictly above 100 kW, plus 1,679 at exactly 100 kW) — 169,407 is also the stream total logged by run 16gi9uVtQ2W7skxT4. It grows by thousands a day. Most of the remainder are small rooftop PV owned by private individuals, which the register anonymises.

Why is 100 kW the default minimum? It is the commercial band. Measured 2026-09-06 on run 16gi9uVtQ2W7skxT4, 89.4% of the units in it belong to named companies (528 of 5,000 fetched rows were anonymised private persons); below 30 kW most rows are anonymised private persons and are not leads.

What is the difference between registered_at and commissioned_at? registered_at is when the unit was entered in the register — the buying trigger. commissioned_at is when it started generating. They are often months apart, and a planned unit has no commissioning date at all — 60.9% fill on run 16gi9uVtQ2W7skxT4 (2,000 rows, ≥ 100 kW), because so many recent registrations are still In Planung.

Can I pull one company's entire portfolio? Yes. Put its ABR… number in operatorMastrNumbers and set maxItems: 0. Verified 2026-09-06, run M2qqeh0ZLRccMBwtx: operator ABR939917736286 returned all 15 of its units in 5.9s.

Can I run this as a daily monitor without paying for the same rows twice? Yes — set registeredLastNDays and turn on dedupeAcrossRuns. The actor remembers up to 250,000 delivered MaStR numbers in a named key-value store, so a unit is delivered and billed once, ever. Verified end to end on 2026-09-06, build 0.1.13: run dAhoz90Pf2cl6y8Nr delivered and billed 413 rows over a 7-day window; run DAu92LgWF5QCubg9s immediately after it, same input, matched the same 456 units, skipped 413 as already delivered and billed nothing at allunit-scraped: 0, operator-enriched: 0, 0 items. The memory lives in a named key-value store, mastr-energy-operator-scraper-seen, on your account; delete its seen-mastr-numbers record to start a fresh list.

Why does ticking three energy carriers make more requests? Because the register mis-parses ~or~. Each value becomes its own request stream and the actor merges them, deduplicating on the MaStR number and sharing your row budget fairly between them. Verified 2026-09-06 on run MYUeAio54ffqDGqIN: three states became three request streams over 654 / 511 / 142 matching units and delivered 131 rows in total, all unique — not 131 rows of the first state. Rows fetched but discarded as duplicates are never billed.

Some filters seem slow. Why? Gemeinde, Gemeindeschlüssel, Landkreis, Straße and Gemarkung are text columns the register scans across all 9.4 million rows. Re-measured 2026-09-06: Gemeinde~contains~'Leipzig' on its own took 8.7s and Landkreis~contains~'Nordfriesland' took 9.7s. Combine them with a capacity, state or date filter and the same query returns in 2.6s.

Which filters cannot be echoed back on the row? The register lets you filter on Bundesland, Land, Landkreis, Gemeinde, Gemeindeschlüssel, Straße, Gemarkung and coordinates, but it does not return them in the grid response. They work as filters; they just will not appear as columns. Town, postcode and cadastral parcel do come back.

Which enrichment columns are empty even with enrichment ON? Three: operator_market_roles, operator_bnetza_number and operator_activity_end. All three measured 0 of 300 on run RFDaZWjI6gs9oYMQv — the register returns them empty. (The last one is semantically explicable: operator_activity_status was Aktiv on 300/300 rows, and an active company has no activity end date.) They are kept as columns so the schema matches the source, not because there is data hiding in them.

Does it ever return the wrong rows? It fails the run instead. Every request stream's first page is asserted row-by-row against the filters that produced it, and the run stops with an explicit error if a returned row does not satisfy them — because the one way this register breaks is by ignoring a filter and quietly handing back the whole corpus.

What happens if my filter matches nothing? The run exits cleanly with 0 rows and a log line that names the actual reason. It distinguishes three different zero-row cases and never blames the register for the other two: the register genuinely matched nothing; the register matched rows that the actor then dropped client-side (anonymised private persons, in-run duplicates, or units already delivered by an earlier run — it prints how many and which); or startPage was set past the end of the result set. A genuinely empty result is not an error — the actor only fails when something is actually broken.

I set startPage and got nothing. Why? Because startPage is past the last page for that filter. The actor now says so explicitly — it prints the match count, the page size, how many pages that is, and that there is nothing to fetch from your start page on. Lower startPage, or raise pageSize.

What happens if I set a charge cap? The run stops the moment the cap is reached, and delivery and billing stop together. With enrichment on, the row charges the run still owes are reserved out of the remaining money before any operator lookup is allowed to start, so the cap buys leads rather than being spent on lookups for rows you never receive. Measured today with maxTotalChargeUsd: 0.09 (50 chargeable events) in operator roll-up mode with enrichment on: 24 rows and 24 lookups delivered, run ZygWWPzB0kH2dbFmF; at maxItems: 500 under a $0.50 cap, 138 rows and 138 lookups, run htYbL07KW9XiHxqQw. If the cap is too small to pay for even one complete lead the run says exactly that in the log — it names the cap as the cause, prints what a row and a lookup each cost, and does not blame the register or your filters.

In operator roll-up mode, do the totals cover the company's whole portfolio? Yes — that is the point of the mode, and it is not a sum of whatever the crawl happened to fetch. maxItems caps companies, so the crawl stops as soon as it has that many; the actor then re-runs your filter with each of those companies' ABR… number appended, pages through everything it matches, and aggregates over that. unit_count, total_gross_capacity_kw, total_net_capacity_kw, largest_unit_kw, the carrier/status/town lists and the first/last registration dates all describe the full matching portfolio. Every row carries portfolio_complete; on the rare row where the re-read could not finish (run timeout, a failed register request, a portfolio above 100,000 units, a row with no operator MaStR number to look the portfolio up by, or the register answering that company with nothing) portfolio_incomplete_reason says which, and the totals then cover only what was fetched. Measured 2026-09-06, run tiAvtCfPjcZc5edNQ: 2,000 unit rows scanned, 500 companies kept, 1,662 units re-read to build their totals, 500 of 500 rows portfolio_complete: true.

Why does a roll-up row list fewer units than it counts? Because unit_count and the totals describe the portfolio, while the nested units array describes what this run delivered — and three things can make the array narrower: dedupeAcrossRuns (units an earlier run already gave you), excludeAnonymised, and the 1,000-unit cap that stops one row outgrowing a dataset item. units_listed is how many units are in the array, units_withheld is unit_count − units_listed, and units_withheld_reason names the cause in words. The counted-but-not-listed units are real portfolio units — they are not missing data and portfolio_complete stays true.

This is a fix, not a feature. Build 0.1.14 applied those two switches before aggregating and then still reported portfolio_complete: true. Measured 2026-09-07: run g3bQ2wN1PvWcdxX7e delivered 20 of Alterric Deutschland GmbH's units, then run J3PzyvppVTore7hYE (operator shape, same filter, dedupeAcrossRuns: true) reported that company as 121 units / 686,988 kW, portfolio_complete: true, portfolio_incomplete_reason: null — where the register answers the very same filter with 141 units / 824,588 kW. On 0.1.15 the equivalent pair (lTdBfL0afagiSAMAw then mkuRZz8jwPIW2lKYy, RWE Wind Onshore & PV Deutschland GmbH) reports unit_count 249 · 1,221,516.87 kW gross · 1,189,990.2 kW net — matching an independent register probe of the same filter exactly — with units_listed 229, units_withheld 20 and the reason naming dedupeAcrossRuns. The 229 listed plus the 20 the earlier run had already delivered are exactly the register's 249, with no overlap.

Do I need residential proxies? No. Datacenter proxies returned identical data to residential-DE and to a direct connection across the whole ladder. Residential is still selectable if you want it.