# Marktstammdatenregister Scraper — MaStR Operator Leads (`scrapersdelight/mastr-energy-operator-scraper`) Actor

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.

- **URL**: https://apify.com/scrapersdelight/mastr-energy-operator-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

from $1.80 / 1,000 per row returneds

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

## ⚡ 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):

| Field | Fill | What it is |
|---|---|---|
| `mastr_number` | **100%** | The official Bundesnetzagentur unit id (`SEE902744940317`) — the stable dedupe key |
| `unit_name` | **100%** | The display name the operator gave it ("Gasturbinen Kraftwerk Leipheim 20", "Solarpark Nord") |
| `gross_capacity_kw` / `net_capacity_kw` | **100%** | Bruttoleistung / Nettonennleistung, in kW |
| `energy_carrier` | **100%** | 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_at` | **100%** | 🔔 The buying trigger — when the unit entered the register |
| `last_updated_at` | **100%** | Catches *changes* to existing assets that a registration filter never sees |
| `unit_type_id` | **100%** | The register's internal unit-type code |
| `unit_detail_url` | **100%** | Deep link to the unit's public page on the register |
| `operator_name` | **100%** | 🏢 The company that owns it |
| `operator_mastr_number` | **100%** | Its `ABR…` registry id — pull its whole portfolio with one input field |
| `operator_profile_url` | **100%** | Deep link to the operator's public register page |
| `operator_person_type` / `is_anonymised` / `operator_enriched` | **100%** | Always present flags |
| `source_filter` / `scraped_at` | **100%** | Which request stream produced the row, and when |
| `town` / `postcode` | **99.5%** | Where the installation sits |
| `location_id` | 61.0% | Grid-connection location id — groups units on one connection point |
| `commissioned_at` | 60.9% | When the asset actually went live (a planned unit has none) |
| `solar_module_count` | 49.4% | PV only — a proxy for physical array size |
| `cadastral_parcel` | 39.8% | Flurstück, the land parcel |
| `is_pilot_wind` | 1.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`):

| Field | Fill | What it is |
|---|---|---|
| `operator_legal_name` | **100%** | The registered legal name |
| `operator_street` | **100%** | Head-office street |
| `operator_house_number` | 98.7% | |
| `operator_postcode` / `operator_town` | **100%** | Often a *different* town from the asset — this is where the decision-maker sits |
| `operator_state` / `operator_country` | **100%** | |
| `operator_market_function` / `operator_activity_status` | **100%** | Market role, and whether the company is still active (`Aktiv` on 300/300) |
| `operator_registered_at` / `operator_last_updated_at` | **100%** | How long the company has been in the register |
| `operator_industry_group_id` / `operator_region_id` | **100%** | The register's industry-group and NUTS-II region codes |
| `operator_is_sme` | 95.7% | Mittelstand vs enterprise — 89 of the 300 rows were SMEs |
| `operator_register_court` | 92.3% | Handelsregister court (e.g. "Essen") |
| `operator_register_number` | 92.0% | HRA / HRB number (e.g. "HRB24353") — the key that joins to Handelsregister, Northdata, Creditreform |
| `operator_vat_id` | 79.3% | USt-IdNr. (e.g. "DE114216469") |
| `operator_activity_start` | 69.3% | When the company started trading in this market role |
| `operator_acer_code` | 51.0% | EU ACER market-participant code |
| `operator_market_roles` · `operator_bnetza_number` · `operator_activity_end` | **0%** | 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:

```json
{
  "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 developers** — `In 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:

```json
{
  "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:

```json
{
  "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:

```json
{
  "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:

```json
{ "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:

```json
{
  "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

| Event | Price | When it fires |
|---|---|---|
| **Per row returned** | **$0.0018** | Once per row written to your dataset |
| **Per operator enriched** | **$0.0018** | Once 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.

***

### ⚖️ Legal and fair use

- **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 all** —
`unit-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.

# Actor input Schema

## `minGrossCapacityKw` (type: `integer`):

Only return units at or above this gross capacity (Bruttoleistung). 100 kW is the commercial band — 169,407 units on 2026-09-06 (run 16gi9uVtQ2W7skxT4). Set 0 or leave empty for no minimum (that is the whole 9.4-million-row register).

## `maxGrossCapacityKw` (type: `integer`):

Only return units at or below this gross capacity. Leave empty for no ceiling.

## `minNetCapacityKw` (type: `integer`):

Optional. Same as above but against Nettonennleistung (net nominal capacity).

## `maxNetCapacityKw` (type: `integer`):

Optional net-capacity ceiling.

## `capacityBoundsInclusive` (type: `boolean`):

The register's API has no ≥ or ≤ operator — ~gte~ and ~lte~ both return an error — so bounds are natively exclusive. With this on, the actor nudges each bound by 0.000001 to reproduce ≥ / ≤ exactly. Re-measured 2026-09-06: >100 kW returns 167,728 units and =100 kW returns 1,679, while >99.999999 returns 169,407 — precisely the sum. Turn off for strictly greater / strictly less.

## `energyCarriers` (type: `array`):

Energieträger. Leave empty for every technology.

## `operatingStatus` (type: `array`):

Betriebs-Status. "In Planung" is the project-pipeline lead (an asset that is not built yet, so the operator is still buying). "Endgültig stillgelegt" is the repowering lead. Leave empty for all four.

## `pilotWindOnly` (type: `boolean`):

Pilotwindenergieanlage — 114 units in the whole register (re-measured 2026-09-06). Niche, but it is the R\&D/prototype segment. Note the register populates the is\_pilot\_wind column sparsely: on a general run it is null on ~99% of rows rather than false.

## `minSolarModules` (type: `integer`):

Anzahl der Solar-Module. PV only; other technologies leave the field empty and are excluded when you set this.

## `maxSolarModules` (type: `integer`):

Optional ceiling on the module count.

## `registeredLastNDays` (type: `integer`):

Shorthand for "registeredAfter = today − N". The natural setting for a scheduled monitor: at or above 100 kW, 1,449 units were registered in the 30 days to 2026-09-06 and 456 in the last 7 (runs kxGaTaHQjsA6HjnEh and dAhoz90Pf2cl6y8Nr). Leave empty to ignore.

## `registeredAfter` (type: `string`):

Registrierungsdatum der Einheit, ISO YYYY-MM-DD. This is the registration date (EinheitMeldeDatum), not the commissioning date.

## `registeredBefore` (type: `string`):

Upper bound on the registration date, ISO YYYY-MM-DD.

## `commissionedAfter` (type: `string`):

Inbetriebnahmedatum der Einheit, ISO YYYY-MM-DD — when the asset actually went live.

## `commissionedBefore` (type: `string`):

Upper bound on the commissioning date, ISO YYYY-MM-DD.

## `updatedAfter` (type: `string`):

Letzte Aktualisierung, ISO YYYY-MM-DD. Catches CHANGES to existing assets — an operator swap, a status change, a capacity correction — which a registration-date filter never sees.

## `updatedBefore` (type: `string`):

Upper bound on the last-updated date, ISO YYYY-MM-DD.

## `bundeslaender` (type: `array`):

Bundesland. Leave empty for all of Germany. "Ausschließliche Wirtschaftszone" is the offshore Exclusive Economic Zone.

## `countries` (type: `array`):

Land. The register is German, so ~99.999% of units are Deutschland — exactly 110 sat abroad when measured 2026-09-06. Leave empty for all.

## `town` (type: `string`):

Ort — the town the installation sits in (not the operator's head office).

## `postcode` (type: `string`):

Postleitzahl. With match mode "contains" a prefix like "04" covers a whole region; with "exactly" it pins one code.

## `district` (type: `string`):

Landkreis, matched as "contains". Filterable but not returned by the register, so it cannot be echoed back on the row. Measured 2026-09-06: a text filter on its own scans the whole 9.4-million-row register and took 8.7s (Gemeinde) to 9.7s (Landkreis) per request. Pair it with a capacity, state or date filter and the same query comes back in 2.6s.

## `municipality` (type: `string`):

Gemeinde. Filterable but not returned on the row. Measured 2026-09-06: a text filter on its own scans the whole 9.4-million-row register and took 8.7s (Gemeinde) to 9.7s (Landkreis) per request. Pair it with a capacity, state or date filter and the same query comes back in 2.6s.

## `municipalityKey` (type: `string`):

The official AGS municipality key. Filterable but not returned on the row. Measured 2026-09-06: a text filter on its own scans the whole 9.4-million-row register and took 8.7s (Gemeinde) to 9.7s (Landkreis) per request. Pair it with a capacity, state or date filter and the same query comes back in 2.6s.

## `street` (type: `string`):

Straße, matched as "contains" (e.g. "Industriestr"). Filterable but not returned on the row. Measured 2026-09-06: a text filter on its own scans the whole 9.4-million-row register and took 8.7s (Gemeinde) to 9.7s (Landkreis) per request. Pair it with a capacity, state or date filter and the same query comes back in 2.6s.

## `cadastralParcel` (type: `string`):

Flurstück, matched as "contains". Populated on roughly 40% of rows.

## `cadastralDistrict` (type: `string`):

Gemarkung, matched as "contains". Filterable but not returned on the row. Measured 2026-09-06: a text filter on its own scans the whole 9.4-million-row register and took 8.7s (Gemeinde) to 9.7s (Landkreis) per request. Pair it with a capacity, state or date filter and the same query comes back in 2.6s.

## `textMatchMode` (type: `string`):

How the town, postcode, municipality and municipality-key filters are matched. District, street and cadastral fields are always "contains".

## `latMin` (type: `string`):

Optional bounding box, decimal degrees (e.g. "53.5") — the south edge. Koordinate: Breitengrad. The register filters on it but does not return it, so it cannot be echoed back on the row.

## `latMax` (type: `string`):

Optional bounding box, decimal degrees (e.g. "53.5") — the north edge. Koordinate: Breitengrad. The register filters on it but does not return it, so it cannot be echoed back on the row.

## `lonMin` (type: `string`):

Optional bounding box, decimal degrees (e.g. "53.5") — the west edge. Koordinate: Längengrad. The register filters on it but does not return it, so it cannot be echoed back on the row.

## `lonMax` (type: `string`):

Optional bounding box, decimal degrees (e.g. "53.5") — the east edge. Koordinate: Längengrad. The register filters on it but does not return it, so it cannot be echoed back on the row.

## `operatorNameContains` (type: `string`):

Name des Anlagenbetreibers — e.g. "Stadtwerke", "Bürgerenergie", "GmbH & Co. KG". By the register's own design this filter only ever matches ORGANISATIONS, never private individuals.

## `unitNameContains` (type: `string`):

Anzeige-Name der Einheit — e.g. "Solarpark", "Windpark", "BHKW", "Batteriespeicher".

## `unitMastrNumbers` (type: `array`):

Exact lookup of specific installations by their SEE… number. One request per number.

## `operatorMastrNumbers` (type: `array`):

Exact lookup by operator ABR… number — returns that operator's ENTIRE portfolio of units. One request per number.

## `startUrls` (type: `array`):

Paste marktstammdatenregister.de grid URLs and the actor reuses their filter, sort and pageSize verbatim. When set, every filter field above is IGNORED. A pasted filter that uses ~or~ or parentheses is REFUSED, because the register mis-parses both and silently returns all 9.4 million rows.

## `enrichOperator` (type: `boolean`):

OFF by default. While it is off, all 22 operator\_\* enrichment columns are null on every row and operator\_enriched is false — measured 0 of 2,000 rows filled on run 16gi9uVtQ2W7skxT4 (2026-09-06). The unit's operator NAME, ABR number and register profile URL come back either way. Switch this on to add street, house number, postcode, town, state, country, Registergericht, Registernummer, VAT ID, SME flag, market function, activity status and dates, industry-group and region ids and the ACER code. Measured on run RFDaZWjI6gs9oYMQv (300 rows, 124 companies, 2026-09-06): legal name / street / postcode / town / state / country / market function / activity status 100%, house number 98.7%, SME flag 95.7%, Handelsregister court 92.3% and number 92.0%, VAT ID 79.3%, ACER code 51.0%. operator\_market\_roles, operator\_bnetza\_number and operator\_activity\_end come back empty from the register itself (0 of 300). Billed per UNIQUE operator looked up and cached — those 300 unit rows collapsed to 124 lookups — and a lookup that resolves to nothing is not billed. With a charge cap set, the row charges the run still owes are reserved before any lookup starts, so the cap buys leads and not lookups (run ZygWWPzB0kH2dbFmF: a $0.09 cap in operator mode delivered 24 rows against 24 lookups).

## `excludeAnonymised` (type: `boolean`):

The Bundesnetzagentur anonymises private individuals itself: the row arrives with IsAnonymisiert = true and the name literally "(natürliche Person)". Those are not business leads, and they are 10.6% of rows even above 100 kW — 528 of a 5,000-row ≥ 100 kW page, measured 2026-09-06 on build 0.1.13, run 16gi9uVtQ2W7skxT4. Leave on unless you specifically need the complete register.

## `outputShape` (type: `string`):

"Unit" gives one row per installation. "Operator" collapses them into one row per company with its units nested and its capacity totalled — measured 2026-09-06 on run RFDaZWjI6gs9oYMQv, 300 unit rows collapse to 124 companies — so this is a smaller, cheaper, sales-ready list. The totals are NOT a sum of whatever the crawl fetched, and NOT a sum of whatever this run delivered: once maxItems has picked which companies you get, the actor re-reads each of those companies' FULL set of units matching your filter and aggregates over that BEFORE dedupeAcrossRuns / excludeAnonymised decide which units get listed, so unit\_count and the kW totals are the real portfolio (run mkuRZz8jwPIW2lKYy, 2026-09-07: unit\_count 249 and 1,221,516.87 kW gross for RWE Wind Onshore & PV Deutschland GmbH, matching an independent register probe of the same filter exactly, on a run where an earlier run had already taken 20 of those units). Every row carries portfolio\_complete, and portfolio\_incomplete\_reason names the cause on the rare row where that re-read could not finish. units\_listed says how many units are in the nested array, which is capped at 1,000 per row; units\_withheld and units\_withheld\_reason account for every unit that is counted in the totals but not listed in the array.

## `maxItems` (type: `integer`):

Hard cap on rows delivered AND billed — rows in unit mode, COMPANIES in operator roll-up mode. Set 0 for everything your filter matches — but note the register held 9,417,097 units on 2026-09-06 (169,407 at or above 100 kW), so an uncapped run is a real bill.

## `sortBy` (type: `string`):

Newest registration first is the default, which is what a lead monitor wants.

## `sortOrder` (type: `string`):

Descending puts the newest registrations (or the largest units, depending on the sort field) first.

## `dedupeAcrossRuns` (type: `boolean`):

Remembers every MaStR number already delivered in a named key-value store, so a scheduled monitor returns — and bills — only genuinely NEW registrations. Keeps the last 250,000 numbers.

## `dateFormat` (type: `string`):

The register emits ASP.NET "/Date(1788480000000)/" strings. "ISO" converts them to full UTC timestamps, "Date only" to YYYY-MM-DD, "Raw" leaves them untouched, "Epoch ms" gives the integer.

## `normaliseNames` (type: `boolean`):

34.5% of operator names in the register carry a FULLWIDTH AMPERSAND ("GmbH ＆ Co. KG") — 689 of a 2,000-name sample taken 2026-09-06, against only 37 plain "&". It is the only fullwidth character observed in the register and it breaks exact-name matching against Handelsregister or CRM data, so this folds it back to "&" and collapses runs of whitespace. Turn off to get the register's bytes verbatim.

## `includeRawJson` (type: `boolean`):

Adds raw\_unit (and raw\_operator when enrichment is on) to every row — the untouched JSON as the register returned it. Useful for auditing a field this actor does not map.

## `startPage` (type: `integer`):

Resume a deep crawl from a given page instead of page 1. If it lands past the last page for your filter, that request stream is skipped with a log line naming the match count and the page count — the run does not fail and nothing is billed.

## `maxPages` (type: `integer`):

Bound how deep each stream goes. 0 = no page limit (row delivery is still bounded by Max rows).

## `pageSize` (type: `integer`):

20–5000. Larger pages mean far fewer requests: a 5,000-row page was measured dupe-free and took 5.6s on 2026-09-06, so the full 169,407-unit commercial band is ~34 requests. Automatically reduced when Max rows is small.

## `maxConcurrency` (type: `integer`):

Parallel requests per stream. 4 is the default and is both polite and fast: 26 platform runs on 2026-09-06 made 1,601 operator-lookup calls plus every list page at this setting and needed 6 retries in total, every one recovered on the next attempt. 8 is available if you want it.

## `requestTimeoutSecs` (type: `integer`):

Per-request timeout. A 5,000-row page took 7.1s on 2026-09-06 and a lone text-column filter 8–10s, so leave headroom.

## `maxRetries` (type: `integer`):

Retries on a transport error with a fresh proxy session. Measured 2026-09-06: 6 transport retries (proxy 595 ECONNRESET / TLS resets) across 26 platform runs, every one cleared on the next attempt. A query the register itself rejects is never retried.

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

The default is Apify's automatic proxy, which resolved to datacenter IPs on every run measured here (run 16gi9uVtQ2W7skxT4, 2026-09-06, billed 0 GB of residential transfer). The register has no anti-bot, no rate limit and no country block: 13 platform runs on build 0.1.13 on 2026-09-06 needed ZERO retries across 1,023 operator lookups plus every list page and every portfolio-completion request, and a direct home-IP connection returns identical totals. Residential is selectable but was never needed.

## Actor input object example

```json
{
  "minGrossCapacityKw": 100,
  "capacityBoundsInclusive": true,
  "pilotWindOnly": false,
  "registeredLastNDays": 30,
  "textMatchMode": "contains",
  "enrichOperator": false,
  "excludeAnonymised": true,
  "outputShape": "unit",
  "maxItems": 50,
  "sortBy": "EinheitMeldeDatum",
  "sortOrder": "desc",
  "dedupeAcrossRuns": false,
  "dateFormat": "iso",
  "normaliseNames": true,
  "includeRawJson": false,
  "startPage": 1,
  "maxPages": 0,
  "pageSize": 1000,
  "maxConcurrency": 4,
  "requestTimeoutSecs": 90,
  "maxRetries": 3,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `units` (type: `string`):

MaStR number, unit name, gross and net capacity, energy carrier, operating status, registration and commissioning dates, town, postcode, operator company and register links — plus operator address, Handelsregister court and number and VAT ID when enrichment is on.

# 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 = {
    "minGrossCapacityKw": 100,
    "registeredLastNDays": 30,
    "maxItems": 50,
    "sortBy": "EinheitMeldeDatum"
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/mastr-energy-operator-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 = {
    "minGrossCapacityKw": 100,
    "registeredLastNDays": 30,
    "maxItems": 50,
    "sortBy": "EinheitMeldeDatum",
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/mastr-energy-operator-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 '{
  "minGrossCapacityKw": 100,
  "registeredLastNDays": 30,
  "maxItems": 50,
  "sortBy": "EinheitMeldeDatum"
}' |
apify call scrapersdelight/mastr-energy-operator-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapersdelight/mastr-energy-operator-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/BH7Qid9bK3Z4sfXke/builds/zd5DlyFrCBuUWgdxZ/openapi.json
