{
  "openapi": "3.1.0",
  "info": {
    "title": "Wisnots Netzanmeldung API",
    "version": "0.1.0-draft",
    "summary": "Register an installation with the responsible German grid operator, whichever one it is.",
    "description": "## What this is\n\nRegistering an installation with the responsible German grid operator: photovoltaics, a heat pump, a wallbox, and with them the § 14a EnWG registration as a steuerbare Verbrauchseinrichtung wherever it applies.\n\n## One route per Anlagentyp, one case model behind them\n\n`POST /v1/anmeldungen/{typ}` names what you are registering: `pv`, `speicher`, `waermepumpe`, `ladeeinrichtung`, `klimaanlage`.\n\nThe path follows the national data set's own axis, the Anlagentypen, rather than the legal regime. § 14a reaches four of those types and not PV, and its threshold is a number in a law that has already been rewritten once, so routing by type keeps your URL stable across a change to the regime. It also means the path pins `device.category`: you do not send it and cannot get it wrong, and `GET /v1/anmeldungen/{typ}/fields` beside it comes back already scoped to that type.\n\n`POST /v1/anmeldungen/14a` remains, for a case opened before anyone has decided which device it is about. It is the one create path that does not pin a category, so there you declare it yourself.\n\nWhat any of them creates is an ordinary Anmeldung. Everything afterwards, reading, correcting, adding material, filing, the event log, happens on `/v1/anmeldungen/{id}` and is identical for every case, whatever created it. Adding an Anlagentyp later is therefore one new create call and nothing else on this page.\n\nCase ids always carry a prefix, `anm_` live and `anm_test_` in the sandbox. That is what keeps a static type segment unambiguous against `{id}`, and what makes a key pointed at the wrong plane fail on the id rather than three screens deep.\n\n## Two ways to give us the case, and they compose\n\n**Drop everything.** Send `material`: pasted text, customer mail, PDFs, photos. We read it and return the fields we found, each with the quote it came from.\n\n**Declare everything.** Send `declared`: a structured body with as much or as little as you hold. No extraction runs for anything you stated.\n\n**Or both**, which is the ordinary case. A declared value is authoritative and is never overwritten by extraction. Where extraction disagrees with something you declared, or two documents disagree with each other, that becomes an entry in `conflicts` rather than a silent choice, because a misdeclaration to a grid operator is worse than a question.\n\n## Sending a document\n\nFiles go to `POST /v1/anmeldungen/{typ}/upload`, the create route's twin, as `multipart/form-data`: one `file` part per document, `text` for a pasted mail, and the JSON body's own fields (`external_ref`, `intent`, `operator`, and `declared` as JSON text) as further parts. Two files and the customer's mail, one call, and the answer is the new case with all three under `material[]`, already being read:\n\n```\ncurl https://netz-api.wisnots.com/v1/anmeldungen/pv/upload \\\n  -H \"Authorization: Bearer $NETZ_API_KEY\" \\\n  -F file=@rechnung.pdf -F file=@typenschild.jpg \\\n  -F text=<kundenmail.txt -F external_ref=ERP-9001\n```\n\nThe same parts on `POST /v1/anmeldungen/{id}/upload` add to a case that already exists:\n\n```\ncurl https://netz-api.wisnots.com/v1/anmeldungen/{id}/upload \\\n  -H \"Authorization: Bearer $NETZ_API_KEY\" \\\n  -F file=@nachtrag.pdf\n```\n\nUp to 25 MB per file. What a file is comes from its bytes, so whatever media type curl guesses for the part does not matter; a file that is not a PDF, a JPEG, a PNG, a WebP or a HEIC image is a 415 naming it, and nothing of that request is stored. On this page, *Upload files* under each Anlagentyp opens the client with this form and a file picker, so the sandbox reads your own documents from here. The SDK's `cases.createFromFiles` and `cases.upload` send it.\n\n**In JSON.** A file also travels inside the JSON body, base64 in `data`, up to 8 MB of it per file, for a client that already builds a JSON body and would rather not compose a form:\n\n```\ncurl https://netz-api.wisnots.com/v1/anmeldungen/pv \\\n  -H \"Authorization: Bearer $NETZ_API_KEY\" -H \"Content-Type: application/json\" \\\n  -d \"{\\\"intent\\\":\\\"draft\\\",\\\"material\\\":[{\\\"kind\\\":\\\"file\\\",\\\"filename\\\":\\\"rechnung.pdf\\\",\\\"content_type\\\":\\\"application/pdf\\\",\\\"data\\\":\\\"$(base64 < rechnung.pdf | tr -d '\\n')\\\"}]}\"\n```\n\n**By upload target,** for a client that streams bytes and never holds the file. Three calls: ask the case for a target, POST the raw bytes to it, hand back the storage id it answers with.\n\n```\ncurl -X POST https://netz-api.wisnots.com/v1/anmeldungen/{id}/documents \\\n  -H \"Authorization: Bearer $NETZ_API_KEY\"\n# {\"upload_url\":\"https://...token=...\"}\n\ncurl -X POST \"<upload_url>\" -H \"Content-Type: application/pdf\" --data-binary @rechnung.pdf\n# {\"storageId\":\"kg2...\"}\n\ncurl -X POST https://netz-api.wisnots.com/v1/anmeldungen/{id}/material \\\n  -H \"Authorization: Bearer $NETZ_API_KEY\" -H \"Content-Type: application/json\" \\\n  -d '{\"material\":[{\"kind\":\"file\",\"storage_id\":\"kg2...\",\"filename\":\"rechnung.pdf\",\"content_type\":\"application/pdf\"}]}'\n```\n\nThe POST to the target carries the bytes as the body, not a form, with no bearer: the target is the credential, single-use, and expires within the hour. The SDK's `cases.uploadByTarget` makes the three calls for you.\n\n**Text.** A pasted mail or a note goes as the `text` part of a form, or as `{\"kind\":\"text\",\"label\":\"Kundenmail\",\"content\":\"...\"}` in JSON, no encoding.\n\n**Then wait on the case, not on a file.** Material walks `pending`, `reading`, then `read` or `unreadable`, and the case's `status` is `extracting` for as long as anything is in the first two. Act on `case.extraction_completed` from a webhook, or poll `GET /v1/anmeldungen/{id}` until `status` leaves `extracting`; the SDK's `cases.waitForExtraction` does that. Count on about a minute for a one-page PDF, more for a scan or a long one.\n\n**More can be sent until filing,** whatever the status, `ready` included: it is read the same way and the case is resolved again. After filing, `/upload` and `/material` answer 409. A real document is one model call, which is why the sandbox reads 25 a day per account; fixtures are unlimited, live has no cap.\n\n## What comes back\n\n- `fields`, everything resolved, each with `source` and, when extracted, the `evidence` quote behind it.\n- `open`, what is still needed, in the data set's own wording, the words the operators use, each marked with who is expected to know it and whether it blocks filing.\n- `conflicts`, where sources disagree.\n- `notices`, things worth knowing that block nothing.\n\nPartial intake is accepted by design. A case with non-blocking gaps still files; we ask for the rest afterwards.\n\n## Field naming\n\nFields have readable paths such as `site.street`, and every one carries its number in the national grid connection data set (version 3.0), the field set every German operator's portal is built on. Anything the named schema does not cover yet is reachable through `declared.nr` by number, so no field of the standard is ever out of reach.\n\n## The sandbox\n\nA `wnz_test_` key instead of a `wnz_live_` one, and that is the whole switch. **The sandbox differs from live in exactly one place: the irreversible outward act.** Extraction is real, resolution is real, the operator's requirements are real, and the PDF is the real template, so what you build against the sandbox is what happens live.\n\nWhat differs:\n\n- **Nothing is sent to a grid operator, ever.** You choose the outcome instead, with `sandbox_outcome` on `POST /{id}/file`, and how fast it arrives with `sandbox_pace`: `instant` inside the call, or `staged` to watch `submitted`, `waiting_operator` and the answer arrive a few seconds apart, each a webhook.\n- **You are the customer.** A sandbox case carries `signature.link`, the link the signature email would have sent; open it, see the watermarked form your customer would see, and sign it to walk the rest of the sequence.\n- **No credits.** A sandbox case consumes nothing, and is not checked against your balance either.\n- **Every PDF is watermarked** on every page. A sandbox form is otherwise byte-for-byte plausible, and somebody will eventually print one.\n- **Fixtures.** `{\"fixture\": \"wallbox-invoice-de\"}` as material returns a known reading with no model call, identically every time, so a screen under construction is free to rebuild. There is one fixture per Anlagentyp: `wallbox-invoice-de`, `heat-pump-invoice-de`, `pv-invoice-de`, `battery-invoice-de`, `air-conditioning-invoice-de`. Real uploads still run real extraction, capped at 25 documents per account per day; over that is a 429 naming the limit, never a silent stub.\n- **Thirty days.** Sandbox cases and everything under them are deleted after 30 days. Integrators paste real customer PDFs into sandboxes; this is why yours will not still be there.\n- **A sandbox operator.** `\"operator\": \"sandbox\"` is fictional and its form never changes, so your tests do not break on the day a real operator reissues theirs.\n\nThe two planes never mix. A key of one mode cannot read, patch, file or discard a case of the other, and one page of `GET /v1/anmeldungen` holds one mode. There is no promotion: a test case never becomes a live one.\n\n## Quickstart\n\n1. Sign in at https://www.wisnots.com/netzanmeldung/portal, switch to Sandbox, and create a key under Entwickler. It starts with `wnz_test_`.\n2. `POST /v1/anmeldungen/ladeeinrichtung` with `{\"intent\":\"draft\",\"material\":[{\"fixture\":\"wallbox-invoice-de\"}]}`. The answer is a case with `status: \"needs_input\"` and `open[]` naming what is still missing, each entry with who is expected to know it.\n3. `PATCH /v1/anmeldungen/{id}` with `{\"declared\": {...}}` for the blocking entries of `open[]`. The case comes back `ready`.\n4. `POST /v1/anmeldungen/{id}/file` with `{\"sandbox_outcome\":\"accepted\",\"sandbox_pace\":\"staged\"}`.\n5. Register a webhook with `POST /v1/webhooks`, or poll `GET /v1/anmeldungen/{id}/events`, and watch `case.status_changed` arrive three times: `submitted`, `waiting_operator`, then the answer.\n6. Switch to Live, create a `wnz_live_` key, and send the same calls without the sandbox fields. Nothing else changes: not the base URL, not the ids' shape, not the responses.\n\n## For your agent\n\nCopy the prompt behind *Set up your agent*, at the top of this page and in the console under Entwickler, into any coding agent. It points the agent at https://www.wisnots.com/netz-api/llms.txt, the skill at https://www.wisnots.com/netz-api/skills/wisnots-netz/SKILL.md and this OpenAPI document, and tells it to build on the sandbox first and to ask you for a key rather than invent one.\n\n## One case, one Anlagentyp\n\nA case registers one Anlagentyp of the national data set. A roof with PV and a battery is two cases, `pv` and `speicher`, which is how the standard and the operators count them too. Give both the same `external_ref` and they stay together in your system and in the console. Several units of one type on one connection are one case with `device.count`, or `inverters[]` where the standard asks per unit.\n\n## Filing for many installers\n\nA platform that files for many installer firms under one key names the firm on every case with `installer_ref`, its own stable id for that firm. Each installer has its own login at each grid operator, and a case is filed with its installer's login there and nobody else's.\n\nOnce a case names its installer, `operator_login` carries the installer's link: a page where that installer hands in grid operator, login and password, encrypted, as many as it has. It is the same link for every case of that installer. Send it when `operator_login.status` is `missing` or `unusable`, which is also when `case.operator_login_missing` fires; the console shows it on the case too. A login handed in never replaces an earlier one, and we can revoke a link without losing what came through it. The installer needs no account and gives no consent: filing the case is the consent.\n\n## Limits\n\n- Material: at most 30 pieces of material per call and 30 files per case. A multipart `file` up to 25 MB, inline `data` up to 8 MB of base64 per file, text up to 100,000 characters.\n- Reading: a photograph over the reader's limit is shrunk; a PDF over it is read a few pages at a time, up to 60 pages. A single page that alone exceeds the limit is the one thing that stays unreadable, and the material says so.\n- Declared: 256 KB per request, 12 levels deep.\n- Rate: 60 requests in a burst per key, refilled at one a second. A refusal is a 429 with `Retry-After`.\n- Sandbox: 25 real documents read per account per day, fixtures unlimited; every sandbox case and everything under it deleted after 30 days.\n- The request log you see in the console keeps 30 days on the sandbox and 90 days live, and never a live case's values.\n\n## Versioning\n\nThe path carries `/v1`. Within v1 we add fields, event types and enum values without notice and never rename or remove one; a rename or a removal is a `/v2`, and v1 keeps working. Unknown request fields are refused, so a client meets a new field only when it asks for it, and a typo never passes silently. Every change is listed below.\n\n## Changes\n\n- 2026-10-12: `installer_ref` on create and `PATCH`, and `operator_login` on the case: a platform filing for many installers names each case's installer firm, and each installer hands in its own grid operator logins through its link. `case.operator_login_missing` says when one is needed.\n- 2026-10-12: `section_14a.consent` is optional. Filing the case is the consent, and `channel` takes your own words.\n- 2026-10-03: the default of a left-out `connection.planned_commissioning_at` never lies in the past. If the day 14 days after the case was filed with us has passed when we prepare the filing, the case is filed with the operator with the day 14 days after that preparation. A date you declare is still filed as given.\n- 2026-10-02: `connection.planned_commissioning_at` is optional with a stated default. Left out, the case is filed with the operator with the day 14 days after it was filed with us, on Berlin's calendar; filing a case without it agrees to that. A date you declare is filed as given.\n- 2026-09-29: `declarations` on `POST /{id}/file`. Under the edition of 28 September 2026 of the terms, a hand-over releases the grid operator's standard declarations for its case; `\"withhold\"` keeps them open for one case.\n- 2026-09-08: material summaries are what the schema always described: `kind` is `file`, `text` or `fixture` (a file used to come back as `document`), a file carries `filename`, `content_type` and `bytes` (it used to carry only `label`), a fixture carries `fixture`. `reading` is a documented status, and the case is `extracting` for as long as any material is `pending` or `reading`; before this a material being read no longer counted, so a poll on `status` could return one step early.\n- 2026-09-07: `POST /v1/anmeldungen/{typ}/upload` beside every create route, and `POST /{id}/upload` on the case, take files as `multipart/form-data`. `curl -F file=@rechnung.pdf` opens a case with the file on it; what a file is comes from its bytes, not from the part's media type.\n- 2026-09-07: a field's number is `nr` everywhere it appears (`fields[].nr`, `open[].nr`, `conflicts[].nr`, the `/fields` rows), the escape hatch is `declared.nr`, an unnamed field is `nr.NNNN`, and the dictionary version is `3.0`. Before this the key had a different name; no integration had shipped against it.\n- 2026-09-07: `sandbox_pace`; `signature.link` on sandbox cases; a filed case's `status` follows the operator's answer (`confirmed`, `rejected`); `GET /v1/netzbetreiber`; the named `billing`, `builder` and `feed_in` blocks and the rest of `end_customer` and `connection`; a Hilfetext on every field; PDFs over the reader's limit read page by page; a fixture for every Anlagentyp.\n- 2026-09-07: webhooks, and an event log that carries the extraction lifecycle.\n- 2026-09-06: one route per Anlagentyp, all five of the standard's types.\n\n## Webhooks\n\nRegister an endpoint with `POST /v1/webhooks` and we deliver every event on your plane to it, signed. The secret is returned once, when you create the endpoint.\n\n**The one to act on is `case.extraction_completed`.** Reading a document is the slow step, and it is the step after which the case is worth re-reading. A delivery says WHAT changed and carries the case id; it does not carry the case, because a receiver that rebuilt a case from a stream of events would get that wrong and we would both be debugging it. Re-read `GET /v1/anmeldungen/{id}`.\n\n**Delivery is at-least-once.** A timeout after your server committed looks exactly like one before it, so you can be handed the same event twice. Every delivery carries `Wisnots-Event-Id`; key on it.\n\nVerify the signature before you trust a delivery:\n\n```\nWisnots-Signature: t=1788770000,v1=<hex hmac-sha256>\n```\n\nThe MAC is over `${t}.${raw body}` with your endpoint secret, and the raw bytes matter: parse the body after verifying, never before. Reject a delivery whose `t` is more than five minutes from now, or a captured one stays valid forever.\n\nNon-2xx is a failure and is retried five times over about ten minutes. Twenty consecutive events that exhaust their retries disable the endpoint, which then shows `active: false` and a `last_error` on `GET /v1/webhooks`, so a silent endpoint can be told from an idle one.\n\nEndpoints belong to one mode, like keys. A sandbox endpoint never receives a live case's events, so a test loop cannot fire your production fulfilment.\n\nThat is all five of the Anlagentypen the national data set defines. There is no sixth to add without going beyond the standard.",
    "contact": {
      "name": "Wisnots",
      "url": "https://wisnots.com"
    }
  },
  "servers": [
    {
      "url": "https://netz-api.wisnots.com",
      "description": "The API. One host serves both planes; which one you are on comes from your key, not from the URL, so there is no second base to switch to when you go live.\n\nIt is not www.wisnots.com, which serves the web app: a request there returns the page with a 200 and an integrator has no way to tell why nothing works."
    }
  ],
  "tags": [
    {
      "name": "Photovoltaics",
      "description": "A PV array at low voltage, with or without storage.\n\nThe national data set puts 104 fields in scope for this type, 66 of them mandatory: 35 fall due in Antragphase 1, 41 in Antragphase 2 and 17 at Inbetriebnahme. On top of the blocks every type shares it adds Wechselrichter (fields 3001 to 3015), PV-Module (3101 to 3103), Leistungsflussüberwachung (3201 to 3206) and NA-Schutz (3301 to 3305).\n\nTen of the inverter fields are ones the standard marks \"Keine Eingabe durch Nutzende empfohlen\": send the ZEREZ-ID and let the certificate register supply the rest."
    },
    {
      "name": "Battery storage",
      "description": "A battery, on its own or beside a PV array.\n\nThe largest of the standard's five types: 107 fields, 73 mandatory, with 33 in Antragphase 1, 41 in Antragphase 2 and 17 at Inbetriebnahme. Its own block is fields 4001 to 4023 and it shares NA-Schutz (3301 to 3305) with photovoltaics.\n\nIt is the one type that is both a § 14a load and a generation asset. The standard settles that inside the storage block rather than across two families, so a storage case asks one type's questions like every other, and the § 14a fields carry the same paths a wallbox uses."
    },
    {
      "name": "Heat pumps",
      "description": "A heat pump, and with it the § 14a registration as a steuerbare Verbrauchseinrichtung where the installed capacity behind the connection passes 4.2 kW.\n\n90 fields in scope, 59 mandatory: 29 in Antragphase 1, 35 in Antragphase 2 and 15 at Inbetriebnahme. The type block is fields 6001 to 6018.\n\nPosting here pins `device.category`, so you do not send it and cannot get it wrong. `/fields` beside this route returns the heat-pump list only."
    },
    {
      "name": "Wallboxes and charge points",
      "description": "A non-public charge point, and with it the § 14a registration above 4.2 kW. Above the operator's own threshold a Zustimmungsverfahren under § 19 NAV follows, which we raise on the case rather than asking you to know it.\n\n94 fields in scope, 61 mandatory: 29 in Antragphase 1, 36 in Antragphase 2 and 15 at Inbetriebnahme. The type block is fields 5001 to 5020.\n\nPosting here pins `device.category`, and `/fields` beside it is scoped to charge points."
    },
    {
      "name": "Air conditioning",
      "description": "An installation for Raumkühlung, and the § 14a registration that comes with it above 4.2 kW.\n\n88 fields in scope, 58 mandatory: 29 in Antragphase 1, 33 in Antragphase 2 and 15 at Inbetriebnahme. The type block is fields 7001 to 7017.\n\nThe fourth and last of the § 14a devices, and a pure load: no generation, no NA-Schutz and no payout fields, so it asks fewer questions than any other type. Its § 14a fields carry the same paths a wallbox uses."
    },
    {
      "name": "§ 14a, any device",
      "description": "The regime route, kept for callers who open a case before anyone has decided which device it is about, and for the two § 14a devices that do not have a typed route yet.\n\nIt is the one create path that does not pin `device.category`, so you declare it yourself and `/fields` needs `?category=` to narrow. Prefer a typed route where one exists: the URL then carries the fact, and a mistyped category cannot file a wallbox as a heat pump."
    },
    {
      "name": "The case",
      "description": "What a case does after it exists, identical whichever route created it.\n\nRead it, correct it, give it more to read, file it, replay its log. Integrating a second Anlagentyp later is therefore one new create call and nothing else on this page.\n\nThere is no PUT. `PATCH` is partial and additive by design: a whole-resource replace would let one caller silently blank a field another party declared, and on a case that two people and an extractor all write to, that is a filing sent with a value nobody chose to remove."
    },
    {
      "name": "Keys and environments",
      "description": "Getting a key, and which plane it puts you on.\n\nThe plane comes from the key, never from the URL or a request field, so there is no base URL to switch when you go live and no flag to leave set by accident. Case ids say it back to you: `anm_` live, `anm_test_` in the sandbox.\n\nKeys are minted in the console at https://www.wisnots.com/netzanmeldung/portal, under Entwickler, on either plane: any signed-in account can create a sandbox key and a live key. A free account is all it takes; the sandbox is free, reads your own documents, and delivers webhooks, so the whole integration is built before anything real is filed."
    }
  ],
  "x-tagGroups": [
    {
      "name": "Anmeldearten",
      "tags": [
        "Photovoltaics",
        "Battery storage",
        "Heat pumps",
        "Wallboxes and charge points",
        "Air conditioning",
        "§ 14a, any device"
      ]
    },
    {
      "name": "Vorgang",
      "tags": [
        "The case"
      ]
    },
    {
      "name": "Zugang",
      "tags": [
        "Keys and environments"
      ]
    },
    {
      "name": "Netzbetreiber",
      "tags": [
        "Grid operators"
      ]
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "paths": {
    "/v1/netzbetreiber": {
      "get": {
        "operationId": "listOperators",
        "summary": "Which grid operators, and which one here",
        "description": "Every operator we carry and what each can take, and with `plz` which of them serve a postcode.\n\n`operator` on create stays explicit: pass the key this returns. An operator guessed wrong changes which questions a case has to answer, so the API never binds one from an address on its own. A postcode area crosses network boundaries often enough that the answer can be two operators; then `resolved` is false and the choice is your user's, with both names to choose from.\n\nThe same list on both planes, and the same list for every key: which operators exist is neither a secret nor a mode.",
        "tags": [
          "Grid operators"
        ],
        "parameters": [
          {
            "name": "plz",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{5}$"
            },
            "description": "A German postcode, five digits, leading zero included. With it the response is the operators serving that postcode plus `resolved`; without it, every operator."
          }
        ],
        "responses": {
          "200": {
            "description": "Operators.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string",
                            "description": "What to send as `operator` on create."
                          },
                          "name": {
                            "type": "string",
                            "description": "The operator's name, to show your user."
                          },
                          "sandbox": {
                            "type": "boolean",
                            "description": "True for the fictional sandbox operator, whose form never changes. Never resolved from a postcode, and refused on a live key."
                          },
                          "filings": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "kind": {
                                  "type": "string",
                                  "description": "A filing kind, e.g. `section_14a`."
                                },
                                "submission": {
                                  "type": [
                                    "string",
                                    "null"
                                  ],
                                  "enum": [
                                    "email",
                                    "portal",
                                    null
                                  ],
                                  "description": "How this operator takes the filing. Null when we have not recorded it."
                                },
                                "readiness": {
                                  "type": "string",
                                  "enum": [
                                    "none",
                                    "address",
                                    "form",
                                    "mapped",
                                    "verified"
                                  ],
                                  "description": "How far we can take a filing of this kind here, cumulative: `address` means we know where it goes, `form` that we hold the operator's form, `mapped` that we can fill it, `verified` that this operator has accepted one. `none` means a case can be prepared and stops at us."
                                }
                              },
                              "required": [
                                "kind",
                                "submission",
                                "readiness"
                              ]
                            }
                          }
                        },
                        "required": [
                          "key",
                          "name",
                          "sandbox",
                          "filings"
                        ]
                      }
                    },
                    "plz": {
                      "type": "string",
                      "description": "Echoed when given."
                    },
                    "resolved": {
                      "type": "boolean",
                      "description": "Only with `plz`. True when exactly one operator serves it, so `data[0].key` can be sent as `operator` without asking anyone. False for none or for several."
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "422": {
            "description": "`plz` is not a five-digit postcode.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks": {
      "post": {
        "operationId": "createWebhook",
        "summary": "Register an endpoint",
        "description": "Delivers every event on this key's plane to your URL, signed.\n\n**The secret comes back once, here.** We sign with it and you verify with it; nothing needs it readable a second time, and a secret an API re-shows is one that leaks through every screenshot of that call.\n\nThe URL must be https and must not be a loopback or private address: this endpoint makes our server fetch what you give it.",
        "tags": [
          "Keys and environments"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "types": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "case.created",
                        "case.material_read",
                        "case.extraction_completed",
                        "case.updated",
                        "case.status_changed",
                        "case.filed",
                        "case.information_requested",
                        "case.information_response_received",
                        "case.information_response_ready",
                        "case.information_response_submitted",
                        "case.operator_login_missing"
                      ]
                    },
                    "description": "Subscribe to a subset. Omit for every type. An unknown type is refused by name rather than silently never firing."
                  },
                  "description": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The endpoint, and its secret.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string"
                    },
                    "mode": {
                      "type": "string",
                      "enum": [
                        "live",
                        "test"
                      ]
                    },
                    "secret": {
                      "type": "string",
                      "example": "whsec_...",
                      "description": "Shown once. Store it now."
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The URL or the type list was refused.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listWebhooks",
        "summary": "Your endpoints",
        "description": "Every endpoint on this key's plane, with enough delivery state to tell a silent endpoint from an idle one. Never the secret.",
        "tags": [
          "Keys and environments"
        ],
        "responses": {
          "200": {
            "description": "Endpoints.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string",
                            "format": "uri"
                          },
                          "mode": {
                            "type": "string",
                            "enum": [
                              "live",
                              "test"
                            ]
                          },
                          "active": {
                            "type": "boolean",
                            "description": "False once disabled by repeated failure."
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "types": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "enum": [
                                "case.created",
                                "case.material_read",
                                "case.extraction_completed",
                                "case.updated",
                                "case.status_changed",
                                "case.filed",
                                "case.information_requested",
                                "case.information_response_received",
                                "case.information_response_ready",
                                "case.information_response_submitted",
                                "case.operator_login_missing"
                              ]
                            },
                            "description": "Absent means every type."
                          },
                          "description": {
                            "type": "string"
                          },
                          "last_attempt_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "last_status": {
                            "type": "integer",
                            "description": "HTTP status your endpoint answered."
                          },
                          "last_error": {
                            "type": "string"
                          },
                          "disabled_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "consecutive_failures": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks/{webhookId}": {
      "delete": {
        "operationId": "deleteWebhook",
        "summary": "Remove an endpoint",
        "description": "Deliveries stop immediately. An id belonging to the other mode is a 404, not a 403: whether it exists is not this key's business either.",
        "tags": [
          "Keys and environments"
        ],
        "parameters": [
          {
            "name": "webhookId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Gone."
          },
          "404": {
            "description": "No such endpoint on this plane.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/pv": {
      "post": {
        "operationId": "createPvAnmeldung",
        "summary": "Register a PV array",
        "description": "**Three bodies to try.** Under *Request Example* on the right, the selector beside *Test Request* switches between a **fixture** the sandbox reads for free, the **raw** shape (the customer's mail and two files, nothing structured) and **declared** fields mapped from your CRM. Whichever is selected there is what *Test Request* opens with; inside the client the same bodies sit in its sidebar, under this request. Files of your own go to *Upload files*, the operation right after this one, which takes them as a form with a file picker.\n\n**The path pins `device.category` to `pv`.** Leave the field out.\n\nThe generation counterpart of `POST /v1/anmeldungen/14a`, and a separate path because the questions differ: PV wants an inverter block and module capacity where § 14a wants a module choice and a record of consent.\n\n`device.category` is not asked. Choosing this path has already answered it, and the case reports it as `derived`. A `declared.device.category` that is not `pv` is a 422 naming both halves, rather than a case judged by one family's questions and another's answers.\n\nEverything after creation is identical to any other case: `GET`, `PATCH`, `material`, `file` and `events` all live on `/v1/anmeldungen/{id}` and know nothing about families.\n\n### Inverters\n\nFive fields are asked ONCE PER INVERTER; the standard marks them `wiederholt`. Send them as an array:\n\n```json\n{\n  \"declared\": {\n    \"device\": { \"inverter_count\": 2, \"apparent_power_kva\": 20 },\n    \"inverters\": [\n      { \"manufacturer\": \"Fronius\", \"apparent_power_kva\": 10 },\n      { \"manufacturer\": \"SMA\", \"apparent_power_kva\": 10 }\n    ]\n  }\n}\n```\n\nThey come back in `open` and `fields` one per instance, `inverters.1.apparent_power_kva`, so a gap names WHICH inverter is missing what. The dictionary publishes them as templates with `[]` where the index goes; that form is a name, not somewhere to write.\n\nWhat blocks a PV filing is the rated power, which is `Antragphase 1` in the standard: what an operator needs to answer a Netzanschlussbegehren at all. Manufacturer, type and ZEREZ-ID are phase 2: real requirements, later ones. The payout fields (MaStR, tax, bank) are later still and never block: the data set scopes itself to the Netzanschlussprozess, and getting paid is a different process.",
        "tags": [
          "Photovoltaics"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Replaying the same key with the same body returns the original case rather than creating a second one. The same key with a different body is a 409. Stored for 24 hours. Strongly recommended: a duplicate grid registration costs a credit and has to be withdrawn by hand."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateRequest"
              },
              "examples": {
                "fixture": {
                  "summary": "Fixture: a hosted PV invoice, free, no model call",
                  "value": {
                    "external_ref": "ERP-5101",
                    "intent": "draft",
                    "material": [
                      {
                        "fixture": "pv-invoice-de"
                      }
                    ]
                  }
                },
                "raw": {
                  "summary": "Raw: the customer's mail and two files, nothing structured",
                  "description": "The shape a back office actually has: a pasted mail and the attachments, no fields. Every value comes back `extracted`, each with the quote it was read from. The two `data` values here are a PDF header and a JPEG header and nothing after: valid base64, sendable, far too little to be documents, so they read as unreadable and the mail carries the case. Send real files and they read for real, 25 a day on the sandbox.",
                  "value": {
                    "external_ref": "ERP-5103",
                    "intent": "draft",
                    "material": [
                      {
                        "kind": "text",
                        "label": "Kundenmail",
                        "content": "Hallo,\n\nanbei die Rechnung und ein Foto vom Typenschild des Wechselrichters zu unserer neuen PV-Anlage (9,84 kWp, ein Wechselrichter Fronius Symo GEN24 10.0 Plus). Zaehlernummer ist 1ESY1189004412. Bitte melden Sie die Anlage beim Netzbetreiber an, wir moechten die Einspeiseverguetung nach EEG.\n\nViele Gruesse\nInes Kaufmann\nHeinrichstrasse 118, 64283 Darmstadt"
                      },
                      {
                        "kind": "file",
                        "filename": "rechnung-sonnenstrom.pdf",
                        "content_type": "application/pdf",
                        "data": "JVBERi0xLjQK"
                      },
                      {
                        "kind": "file",
                        "filename": "typenschild-wechselrichter.jpg",
                        "content_type": "image/jpeg",
                        "data": "/9j/4AAQSkZJRgAB"
                      }
                    ]
                  }
                },
                "declared": {
                  "summary": "Declared: fields mapped from your CRM, one inverter, 9.84 kWp",
                  "value": {
                    "external_ref": "ERP-5102",
                    "intent": "draft",
                    "declared": {
                      "site": {
                        "street": "Heinrichstrasse",
                        "house_number": "118",
                        "postal_code": "64283",
                        "city": "Darmstadt"
                      },
                      "end_customer": {
                        "first_name": "Ines",
                        "last_name": "Kaufmann",
                        "email": "i.kaufmann@example.de",
                        "phone": "+49 6151 5550188",
                        "is_owner": true
                      },
                      "installer": {
                        "company": "Sonnenstrom Hessen GmbH",
                        "street": "Rheinstrasse",
                        "house_number": "40",
                        "postal_code": "64293",
                        "city": "Darmstadt",
                        "email": "info@sonnenstrom-hessen.example"
                      },
                      "device": {
                        "inverter_count": 1,
                        "apparent_power_kva": 10,
                        "active_power_kw": 10,
                        "module_power_kwp": 9.84,
                        "installed_at": "2026-06-12",
                        "status": "planned"
                      },
                      "inverters": [
                        {
                          "manufacturer": "Fronius",
                          "model": "Symo GEN24 10.0 Plus",
                          "apparent_power_kva": 10,
                          "active_power_kw": 10
                        }
                      ],
                      "protection": {
                        "manufacturer": "Ziehl",
                        "model": "UFR1001E"
                      },
                      "connection": {
                        "meter_number": "1ESY1189004412"
                      },
                      "feed_in": {
                        "sale_form": "feed_in_tariff"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The case as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Case"
                },
                "examples": {
                  "needs_input": {
                    "$ref": "#/components/examples/CaseNeedsInput"
                  },
                  "ready": {
                    "$ref": "#/components/examples/CaseReady"
                  },
                  "filed": {
                    "$ref": "#/components/examples/CaseFiled"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "The body is malformed, or its device belongs to another family.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/pv/upload": {
      "post": {
        "operationId": "uploadPv",
        "summary": "Upload files for a PV array",
        "description": "Opens the case from files, the way a back office has them: the documents, the customer's mail, and whatever fields you already hold. `curl -F file=@rechnung.pdf -F file=@typenschild.jpg -F text=<kundenmail.txt` onto this route answers with the new case and all three under `material[]`, already being read.\n\nOne `file` part per document (a PDF, a JPEG, a PNG, a WebP or a HEIC image, up to 25 MB each), `text` for a pasted mail, and the JSON body's own fields as further parts, `declared` as JSON text. What a file is comes from its bytes, so the part's media type does not matter; a file of any other kind is a 415 naming it, and nothing of the request is stored. Empty parts are ignored, so the form this page's client builds, every row checked, goes through.\n\nThe case that comes back is the same one `POST /v1/anmeldungen/pv` returns, and everything afterwards is identical: read it, correct it, add to it with `POST /{id}/upload`, file it. A JSON body sent here is a 415 naming the create route; the two doors do not overlap, so neither has a second shape to learn.",
        "tags": [
          "Photovoltaics"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Replaying the same key with the same body returns the original case rather than creating a second one. The same key with a different body is a 409. Stored for 24 hours. Strongly recommended: a duplicate grid registration costs a credit and has to be withdrawn by hand."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "One part per document, repeated for each file: a PDF, a JPEG, a PNG, a WebP or a HEIC image, up to 25 MB each. What a file is comes from its bytes, so the part's own media type does not matter; a file of any other kind is a 415 naming it, and nothing of the request is stored."
                  },
                  "text": {
                    "type": "string",
                    "description": "A pasted mail or a note, read like a file. `curl -F text=<kundenmail.txt` sends one."
                  },
                  "external_ref": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "As in the JSON body."
                  },
                  "installer_ref": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "As in the JSON body."
                  },
                  "intent": {
                    "type": "string",
                    "enum": [
                      "draft",
                      "file"
                    ],
                    "default": "draft",
                    "description": "As in the JSON body."
                  },
                  "operator": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9-]{0,62}$",
                    "description": "As in the JSON body."
                  },
                  "declared": {
                    "type": "string",
                    "description": "The JSON body's `declared` object, as JSON text."
                  }
                }
              },
              "encoding": {
                "file": {
                  "contentType": "application/pdf, image/jpeg, image/png, image/webp, image/heic"
                }
              },
              "examples": {
                "multipart": {
                  "summary": "Multipart: two files and the customer's mail",
                  "description": "Pick the files; the rest of the parts are text. The answer is the case with all of it under `material[]`, already being read.",
                  "value": {
                    "text": "Hallo, anbei Rechnung und Typenschild. Zaehlernummer 1ESY1189004412.",
                    "external_ref": "ERP-9001",
                    "intent": "draft"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The case as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Case"
                },
                "examples": {
                  "needs_input": {
                    "$ref": "#/components/examples/CaseNeedsInput"
                  },
                  "ready": {
                    "$ref": "#/components/examples/CaseReady"
                  },
                  "filed": {
                    "$ref": "#/components/examples/CaseFiled"
                  }
                }
              }
            }
          },
          "409": {
            "description": "The idempotency key was reused with a different body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "A file over 25 MB.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "415": {
            "description": "A file the reader cannot take, named; or a JSON body, which belongs on the create route.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "A part was refused, with its name.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/pv/fields": {
      "get": {
        "operationId": "getPvFieldDictionary",
        "summary": "The PV field dictionary",
        "description": "Every field a PV case understands, scoped by the path so no `category` parameter is needed: PV admits exactly one, and a parameter would only be a way to contradict it.\n\nRepeating fields appear once, as templates: `inverters[].manufacturer`. Build one row of your form from each and repeat the row.",
        "tags": [
          "Photovoltaics"
        ],
        "parameters": [
          {
            "name": "lang",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "de",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The dictionary.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "version": {
                      "type": "string"
                    },
                    "surface": {
                      "type": "string",
                      "enum": [
                        "universal",
                        "operator"
                      ]
                    },
                    "operator": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  },
                  "required": [
                    "version",
                    "surface",
                    "data"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/speicher": {
      "post": {
        "operationId": "createSpeicher",
        "summary": "Register a battery storage unit",
        "description": "**Three bodies to try.** Under *Request Example* on the right, the selector beside *Test Request* switches between a **fixture** the sandbox reads for free, the **raw** shape (the customer's mail and two files, nothing structured) and **declared** fields mapped from your CRM. Whichever is selected there is what *Test Request* opens with; inside the client the same bodies sit in its sidebar, under this request. Files of your own go to *Upload files*, the operation right after this one, which takes them as a form with a file picker.\n\nOpens a case for a battery storage unit, on its own or alongside a PV array.\n\n**The path pins `device.category` to `battery`.** Leave the field out; sending a different value is a 400 rather than a silent overwrite.\n\nStorage is its own family, not a § 14a case with a different device, and it is the largest of the standard's types: 107 fields, 73 of them mandatory. It is a controllable load while it charges and a generation asset while it discharges, and the standard resolves that inside its own block rather than across two: fields 4011, 4015, 4016, 4017 and 4023 are the § 14a questions, and they carry the same paths a wallbox uses so a § 14a screen built once does not have to be built again. NA-Schutz (3301 to 3305) is shared with PV.\n\nFour fields block, and they are the standard's own Antragphase 1: `device.count`, `device.apparent_power_kva`, `device.coupling` and `device.operating_mode`. Everything else may follow.\n\nWhat it creates is an ordinary Anmeldung, read and filed on `/v1/anmeldungen/{id}` like any other.",
        "tags": [
          "Battery storage"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Replaying the same key with the same body returns the original case rather than creating a second one. The same key with a different body is a 409. Stored for 24 hours. Strongly recommended: a duplicate grid registration costs a credit and has to be withdrawn by hand."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateRequest"
              },
              "examples": {
                "fixture": {
                  "summary": "Fixture: a hosted storage invoice, free, no model call",
                  "value": {
                    "external_ref": "ERP-5201",
                    "intent": "draft",
                    "material": [
                      {
                        "fixture": "battery-invoice-de"
                      }
                    ]
                  }
                },
                "raw": {
                  "summary": "Raw: the customer's mail and two files, nothing structured",
                  "description": "The shape a back office actually has: a pasted mail and the attachments, no fields. Every value comes back `extracted`, each with the quote it was read from. The two `data` values here are a PDF header and a JPEG header and nothing after: valid base64, sendable, far too little to be documents, so they read as unreadable and the mail carries the case. Send real files and they read for real, 25 a day on the sandbox.",
                  "value": {
                    "external_ref": "ERP-5203",
                    "intent": "draft",
                    "material": [
                      {
                        "kind": "text",
                        "label": "Kundenmail",
                        "content": "Hallo,\n\nim Anhang Rechnung und Typenschild zu unserem Stromspeicher (BYD Battery-Box Premium HVS 10.2, 10,24 kWh, eigener Wechselrichter). Zaehlernummer 1ESY1170551234. Der Speicher soll nach 14a EnWG mit Modul 1 angemeldet werden.\n\nViele Gruesse\nJonas Petersen\nKanalufer 9, 24106 Kiel"
                      },
                      {
                        "kind": "file",
                        "filename": "rechnung-nordkap.pdf",
                        "content_type": "application/pdf",
                        "data": "JVBERi0xLjQK"
                      },
                      {
                        "kind": "file",
                        "filename": "typenschild-speicher.jpg",
                        "content_type": "image/jpeg",
                        "data": "/9j/4AAQSkZJRgAB"
                      }
                    ]
                  }
                },
                "declared": {
                  "summary": "Declared: fields mapped from your CRM, 10.24 kWh, AC-coupled",
                  "value": {
                    "external_ref": "ERP-5202",
                    "intent": "draft",
                    "declared": {
                      "site": {
                        "street": "Kanalufer",
                        "house_number": "9",
                        "postal_code": "24106",
                        "city": "Kiel"
                      },
                      "end_customer": {
                        "first_name": "Jonas",
                        "last_name": "Petersen",
                        "email": "j.petersen@example.de",
                        "phone": "+49 431 5550172",
                        "is_owner": true
                      },
                      "installer": {
                        "company": "Elektro Nordkap GmbH",
                        "email": "kontakt@elektro-nordkap.example"
                      },
                      "device": {
                        "count": 1,
                        "manufacturer": "BYD",
                        "model": "Battery-Box Premium HVS 10.2",
                        "apparent_power_kva": 5,
                        "active_power_kw": 5,
                        "storage_capacity_kwh": 10.24,
                        "coupling": "Eigener Wechselrichter (AC-Kopplung)",
                        "installed_at": "2026-07-02",
                        "status": "planned"
                      },
                      "section_14a": {
                        "is_controllable": true,
                        "control_responsibility": "metering_operator",
                        "modules": "1",
                        "consent": {
                          "given_by": "end_customer",
                          "given_at": "2026-07-01T09:30:00Z",
                          "channel": "email"
                        }
                      },
                      "connection": {
                        "meter_number": "1ESY1170551234"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The case as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Case"
                },
                "examples": {
                  "needs_input": {
                    "$ref": "#/components/examples/CaseNeedsInput"
                  },
                  "ready": {
                    "$ref": "#/components/examples/CaseReady"
                  },
                  "filed": {
                    "$ref": "#/components/examples/CaseFiled"
                  }
                }
              }
            }
          },
          "409": {
            "description": "The idempotency key was reused with a different body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "A declared value was rejected.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "validation_failed": {
                    "$ref": "#/components/examples/ProblemValidation"
                  }
                }
              }
            }
          },
          "402": {
            "description": "No credits left. The case is not created.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Retry-After is set.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/speicher/upload": {
      "post": {
        "operationId": "uploadSpeicher",
        "summary": "Upload files for a battery storage unit",
        "description": "Opens the case from files, the way a back office has them: the documents, the customer's mail, and whatever fields you already hold. `curl -F file=@rechnung.pdf -F file=@typenschild.jpg -F text=<kundenmail.txt` onto this route answers with the new case and all three under `material[]`, already being read.\n\nOne `file` part per document (a PDF, a JPEG, a PNG, a WebP or a HEIC image, up to 25 MB each), `text` for a pasted mail, and the JSON body's own fields as further parts, `declared` as JSON text. What a file is comes from its bytes, so the part's media type does not matter; a file of any other kind is a 415 naming it, and nothing of the request is stored. Empty parts are ignored, so the form this page's client builds, every row checked, goes through.\n\nThe case that comes back is the same one `POST /v1/anmeldungen/speicher` returns, and everything afterwards is identical: read it, correct it, add to it with `POST /{id}/upload`, file it. A JSON body sent here is a 415 naming the create route; the two doors do not overlap, so neither has a second shape to learn.",
        "tags": [
          "Battery storage"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Replaying the same key with the same body returns the original case rather than creating a second one. The same key with a different body is a 409. Stored for 24 hours. Strongly recommended: a duplicate grid registration costs a credit and has to be withdrawn by hand."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "One part per document, repeated for each file: a PDF, a JPEG, a PNG, a WebP or a HEIC image, up to 25 MB each. What a file is comes from its bytes, so the part's own media type does not matter; a file of any other kind is a 415 naming it, and nothing of the request is stored."
                  },
                  "text": {
                    "type": "string",
                    "description": "A pasted mail or a note, read like a file. `curl -F text=<kundenmail.txt` sends one."
                  },
                  "external_ref": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "As in the JSON body."
                  },
                  "installer_ref": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "As in the JSON body."
                  },
                  "intent": {
                    "type": "string",
                    "enum": [
                      "draft",
                      "file"
                    ],
                    "default": "draft",
                    "description": "As in the JSON body."
                  },
                  "operator": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9-]{0,62}$",
                    "description": "As in the JSON body."
                  },
                  "declared": {
                    "type": "string",
                    "description": "The JSON body's `declared` object, as JSON text."
                  }
                }
              },
              "encoding": {
                "file": {
                  "contentType": "application/pdf, image/jpeg, image/png, image/webp, image/heic"
                }
              },
              "examples": {
                "multipart": {
                  "summary": "Multipart: two files and the customer's mail",
                  "description": "Pick the files; the rest of the parts are text. The answer is the case with all of it under `material[]`, already being read.",
                  "value": {
                    "text": "Hallo, anbei Rechnung und Typenschild. Zaehlernummer 1ESY1189004412.",
                    "external_ref": "ERP-9001",
                    "intent": "draft"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The case as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Case"
                },
                "examples": {
                  "needs_input": {
                    "$ref": "#/components/examples/CaseNeedsInput"
                  },
                  "ready": {
                    "$ref": "#/components/examples/CaseReady"
                  },
                  "filed": {
                    "$ref": "#/components/examples/CaseFiled"
                  }
                }
              }
            }
          },
          "409": {
            "description": "The idempotency key was reused with a different body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "A file over 25 MB.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "415": {
            "description": "A file the reader cannot take, named; or a JSON body, which belongs on the create route.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "A part was refused, with its name.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/speicher/fields": {
      "get": {
        "operationId": "getSpeicherFieldDictionary",
        "summary": "Fields for a battery storage unit",
        "description": "Every field that applies to a battery storage unit, with its field number, the standard's own label and help text, its type and option set, when it is required, and who is expected to know it.\n\nAlready scoped by the path, so there is no `category` parameter to get wrong.",
        "tags": [
          "Battery storage"
        ],
        "parameters": [
          {
            "name": "operator",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"
            },
            "description": "Include that grid operator's own requirements, marked `operator_specific`. Has no effect under a universal key, which by design sees the same field list whichever operator it names; see `requirements` on a case."
          },
          {
            "name": "lang",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "de",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The dictionary.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "version": {
                      "type": "string"
                    },
                    "surface": {
                      "type": "string",
                      "enum": [
                        "universal",
                        "operator"
                      ],
                      "description": "Which surface answered you. Echoed so you need not remember which key you used."
                    },
                    "operator": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  },
                  "required": [
                    "version",
                    "surface",
                    "data"
                  ]
                },
                "examples": {
                  "slice": {
                    "$ref": "#/components/examples/FieldDictionarySlice"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/waermepumpe": {
      "post": {
        "operationId": "createWaermepumpe",
        "summary": "Register a heat pump",
        "description": "**Three bodies to try.** Under *Request Example* on the right, the selector beside *Test Request* switches between a **fixture** the sandbox reads for free, the **raw** shape (the customer's mail and two files, nothing structured) and **declared** fields mapped from your CRM. Whichever is selected there is what *Test Request* opens with; inside the client the same bodies sit in its sidebar, under this request. Files of your own go to *Upload files*, the operation right after this one, which takes them as a form with a file picker.\n\nOpens a case for a heat pump from declared fields, raw material, or both.\n\n**The path pins `device.category` to `heat_pump`.** Leave the field out; sending a different value is a 400 rather than a silent overwrite, because a category corrected quietly is a filing sent to the wrong desk.\n\nThe type block is fields 6001 to 6018. Registering it as a steuerbare Verbrauchseinrichtung under § 14a follows from the declared capacity, so there is no second call and no flag to set.\n\nExtraction runs immediately for inline material and the response already reflects it for small inputs; a case still being read comes back as `extracting` and should be polled or awaited by webhook.\n\nWhat it creates is an ordinary Anmeldung. Everything after this happens on `/v1/anmeldungen/{id}` and is the same for every case, whatever opened it.",
        "tags": [
          "Heat pumps"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Replaying the same key with the same body returns the original case rather than creating a second one. The same key with a different body is a 409. Stored for 24 hours. Strongly recommended: a duplicate grid registration costs a credit and has to be withdrawn by hand."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateRequest"
              },
              "examples": {
                "fixture": {
                  "summary": "Fixture: a hosted heat pump invoice, free, no model call",
                  "value": {
                    "external_ref": "ERP-5301",
                    "intent": "draft",
                    "material": [
                      {
                        "fixture": "heat-pump-invoice-de"
                      }
                    ]
                  }
                },
                "raw": {
                  "summary": "Raw: the customer's mail and two files, nothing structured",
                  "description": "The shape a back office actually has: a pasted mail and the attachments, no fields. Every value comes back `extracted`, each with the quote it was read from. The two `data` values here are a PDF header and a JPEG header and nothing after: valid base64, sendable, far too little to be documents, so they read as unreadable and the mail carries the case. Send real files and they read for real, 25 a day on the sandbox.",
                  "value": {
                    "external_ref": "ERP-5303",
                    "intent": "draft",
                    "material": [
                      {
                        "kind": "text",
                        "label": "Kundenmail",
                        "content": "Hallo,\n\nanbei Rechnung und Typenschild unserer Waermepumpe (Vaillant aroTHERM plus VWL 75/6 A, Luft-Wasser). Sie laeuft seit Mai. Zaehlernummer 1ESY1170099887. Bitte Modul 1 nach 14a EnWG anmelden, die Steuerung uebernimmt der Netzbetreiber.\n\nViele Gruesse\nPetra Ahlers\nWiesenweg 3, 22607 Hamburg"
                      },
                      {
                        "kind": "file",
                        "filename": "rechnung-nordlicht.pdf",
                        "content_type": "application/pdf",
                        "data": "JVBERi0xLjQK"
                      },
                      {
                        "kind": "file",
                        "filename": "typenschild-waermepumpe.jpg",
                        "content_type": "image/jpeg",
                        "data": "/9j/4AAQSkZJRgAB"
                      }
                    ]
                  }
                },
                "declared": {
                  "summary": "Declared: fields mapped from your CRM, air to water, 12 A starting current",
                  "value": {
                    "external_ref": "ERP-5302",
                    "intent": "draft",
                    "declared": {
                      "site": {
                        "street": "Wiesenweg",
                        "house_number": "3",
                        "postal_code": "22607",
                        "city": "Hamburg"
                      },
                      "end_customer": {
                        "first_name": "Petra",
                        "last_name": "Ahlers",
                        "email": "p.ahlers@example.de",
                        "phone": "+49 40 5550199",
                        "is_owner": true
                      },
                      "installer": {
                        "company": "Sanitaer & Waerme Nordlicht GmbH",
                        "email": "buero@nordlicht-waerme.example"
                      },
                      "device": {
                        "count": 1,
                        "manufacturer": "Vaillant",
                        "model": "aroTHERM plus VWL 75/6 A",
                        "heat_pump_type": "air_water",
                        "starting_current_a": 12,
                        "installed_at": "2026-05-20",
                        "status": "installed"
                      },
                      "section_14a": {
                        "control_responsibility": "grid_operator",
                        "modules": "1",
                        "consent": {
                          "given_by": "end_customer",
                          "given_at": "2026-05-18T09:30:00Z",
                          "channel": "phone"
                        }
                      },
                      "connection": {
                        "meter_number": "1ESY1170099887"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The case as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Case"
                },
                "examples": {
                  "needs_input": {
                    "$ref": "#/components/examples/CaseNeedsInput"
                  },
                  "ready": {
                    "$ref": "#/components/examples/CaseReady"
                  },
                  "filed": {
                    "$ref": "#/components/examples/CaseFiled"
                  }
                }
              }
            }
          },
          "409": {
            "description": "The idempotency key was reused with a different body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "A declared value was rejected.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "validation_failed": {
                    "$ref": "#/components/examples/ProblemValidation"
                  }
                }
              }
            }
          },
          "402": {
            "description": "No credits left. The case is not created.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Retry-After is set.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/waermepumpe/upload": {
      "post": {
        "operationId": "uploadWaermepumpe",
        "summary": "Upload files for a heat pump",
        "description": "Opens the case from files, the way a back office has them: the documents, the customer's mail, and whatever fields you already hold. `curl -F file=@rechnung.pdf -F file=@typenschild.jpg -F text=<kundenmail.txt` onto this route answers with the new case and all three under `material[]`, already being read.\n\nOne `file` part per document (a PDF, a JPEG, a PNG, a WebP or a HEIC image, up to 25 MB each), `text` for a pasted mail, and the JSON body's own fields as further parts, `declared` as JSON text. What a file is comes from its bytes, so the part's media type does not matter; a file of any other kind is a 415 naming it, and nothing of the request is stored. Empty parts are ignored, so the form this page's client builds, every row checked, goes through.\n\nThe case that comes back is the same one `POST /v1/anmeldungen/waermepumpe` returns, and everything afterwards is identical: read it, correct it, add to it with `POST /{id}/upload`, file it. A JSON body sent here is a 415 naming the create route; the two doors do not overlap, so neither has a second shape to learn.",
        "tags": [
          "Heat pumps"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Replaying the same key with the same body returns the original case rather than creating a second one. The same key with a different body is a 409. Stored for 24 hours. Strongly recommended: a duplicate grid registration costs a credit and has to be withdrawn by hand."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "One part per document, repeated for each file: a PDF, a JPEG, a PNG, a WebP or a HEIC image, up to 25 MB each. What a file is comes from its bytes, so the part's own media type does not matter; a file of any other kind is a 415 naming it, and nothing of the request is stored."
                  },
                  "text": {
                    "type": "string",
                    "description": "A pasted mail or a note, read like a file. `curl -F text=<kundenmail.txt` sends one."
                  },
                  "external_ref": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "As in the JSON body."
                  },
                  "installer_ref": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "As in the JSON body."
                  },
                  "intent": {
                    "type": "string",
                    "enum": [
                      "draft",
                      "file"
                    ],
                    "default": "draft",
                    "description": "As in the JSON body."
                  },
                  "operator": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9-]{0,62}$",
                    "description": "As in the JSON body."
                  },
                  "declared": {
                    "type": "string",
                    "description": "The JSON body's `declared` object, as JSON text."
                  }
                }
              },
              "encoding": {
                "file": {
                  "contentType": "application/pdf, image/jpeg, image/png, image/webp, image/heic"
                }
              },
              "examples": {
                "multipart": {
                  "summary": "Multipart: two files and the customer's mail",
                  "description": "Pick the files; the rest of the parts are text. The answer is the case with all of it under `material[]`, already being read.",
                  "value": {
                    "text": "Hallo, anbei Rechnung und Typenschild. Zaehlernummer 1ESY1189004412.",
                    "external_ref": "ERP-9001",
                    "intent": "draft"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The case as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Case"
                },
                "examples": {
                  "needs_input": {
                    "$ref": "#/components/examples/CaseNeedsInput"
                  },
                  "ready": {
                    "$ref": "#/components/examples/CaseReady"
                  },
                  "filed": {
                    "$ref": "#/components/examples/CaseFiled"
                  }
                }
              }
            }
          },
          "409": {
            "description": "The idempotency key was reused with a different body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "A file over 25 MB.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "415": {
            "description": "A file the reader cannot take, named; or a JSON body, which belongs on the create route.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "A part was refused, with its name.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/waermepumpe/fields": {
      "get": {
        "operationId": "getWaermepumpeFieldDictionary",
        "summary": "Fields for a heat pump",
        "description": "Every field that applies to a heat pump, with its field number, the standard's own label and help text, its type and option set, when it is required, and who is expected to know it.\n\nAlready scoped by the path, so there is no `category` parameter to get wrong. Build your intake form from this rather than hardcoding our schema and it keeps working as the dictionary grows.",
        "tags": [
          "Heat pumps"
        ],
        "parameters": [
          {
            "name": "operator",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"
            },
            "description": "Include that grid operator's own requirements, marked `operator_specific`. Has no effect under a universal key, which by design sees the same field list whichever operator it names; see `requirements` on a case."
          },
          {
            "name": "lang",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "de",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The dictionary.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "version": {
                      "type": "string"
                    },
                    "surface": {
                      "type": "string",
                      "enum": [
                        "universal",
                        "operator"
                      ],
                      "description": "Which surface answered you. Echoed so you need not remember which key you used."
                    },
                    "operator": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  },
                  "required": [
                    "version",
                    "surface",
                    "data"
                  ]
                },
                "examples": {
                  "slice": {
                    "$ref": "#/components/examples/FieldDictionarySlice"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/ladeeinrichtung": {
      "post": {
        "operationId": "createLadeeinrichtung",
        "summary": "Register a charge point",
        "description": "**Three bodies to try.** Under *Request Example* on the right, the selector beside *Test Request* switches between a **fixture** the sandbox reads for free, the **raw** shape (the customer's mail and two files, nothing structured) and **declared** fields mapped from your CRM. Whichever is selected there is what *Test Request* opens with; inside the client the same bodies sit in its sidebar, under this request. Files of your own go to *Upload files*, the operation right after this one, which takes them as a form with a file picker.\n\nOpens a case for a charge point from declared fields, raw material, or both.\n\n**The path pins `device.category` to `charge_point`.** Leave the field out; sending a different value is a 400 rather than a silent overwrite, because a category corrected quietly is a filing sent to the wrong desk.\n\nThe type block is fields 5001 to 5020. The § 14a registration above 4.2 kW, and a Zustimmungsverfahren under § 19 NAV where the operator requires one, follow from the declared capacity and appear as blockers on the case.\n\nExtraction runs immediately for inline material and the response already reflects it for small inputs; a case still being read comes back as `extracting` and should be polled or awaited by webhook.\n\nWhat it creates is an ordinary Anmeldung. Everything after this happens on `/v1/anmeldungen/{id}` and is the same for every case, whatever opened it.",
        "tags": [
          "Wallboxes and charge points"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Replaying the same key with the same body returns the original case rather than creating a second one. The same key with a different body is a 409. Stored for 24 hours. Strongly recommended: a duplicate grid registration costs a credit and has to be withdrawn by hand."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateRequest"
              },
              "examples": {
                "fixture": {
                  "summary": "Fixture: a hosted wallbox invoice, free, no model call",
                  "value": {
                    "external_ref": "ERP-5401",
                    "intent": "draft",
                    "material": [
                      {
                        "fixture": "wallbox-invoice-de"
                      }
                    ]
                  }
                },
                "raw": {
                  "summary": "Raw: the customer's mail and two files, nothing structured",
                  "description": "The shape a back office actually has: a pasted mail and the attachments, no fields. Every value comes back `extracted`, each with the quote it was read from. The two `data` values here are a PDF header and a JPEG header and nothing after: valid base64, sendable, far too little to be documents, so they read as unreadable and the mail carries the case. Send real files and they read for real, 25 a day on the sandbox.",
                  "value": {
                    "external_ref": "ERP-5403",
                    "intent": "draft",
                    "material": [
                      {
                        "kind": "text",
                        "label": "Kundenmail",
                        "content": "Hallo,\n\nanbei die Unterlagen zu meiner Wallbox (Easee Home, 22 kW), Rechnung und Typenschild. Die Anlage laeuft seit 2022, Zaehlernummer 1ESY1161234567. Ich bin Eigentuemer. Bitte Modul 1 nach 14a EnWG anmelden.\n\nViele Gruesse\nThomas Weber\nSchackumer Strasse 20, 40667 Meerbusch"
                      },
                      {
                        "kind": "file",
                        "filename": "rechnung-haslinger.pdf",
                        "content_type": "application/pdf",
                        "data": "JVBERi0xLjQK"
                      },
                      {
                        "kind": "file",
                        "filename": "typenschild-wallbox.jpg",
                        "content_type": "image/jpeg",
                        "data": "/9j/4AAQSkZJRgAB"
                      }
                    ]
                  }
                },
                "declared": {
                  "summary": "Declared: fields mapped from your CRM, 22 kW, controllable",
                  "value": {
                    "external_ref": "ERP-5402",
                    "intent": "draft",
                    "declared": {
                      "site": {
                        "street": "Schackumer Strasse",
                        "house_number": "20",
                        "postal_code": "40667",
                        "city": "Meerbusch"
                      },
                      "end_customer": {
                        "first_name": "Thomas",
                        "last_name": "Weber",
                        "email": "t.weber@example.de",
                        "phone": "+49 2150 5550144",
                        "is_owner": true
                      },
                      "installer": {
                        "company": "Elektro Haslinger GmbH",
                        "email": "info@elektro-haslinger.example"
                      },
                      "device": {
                        "count": 1,
                        "manufacturer": "Easee",
                        "model": "Easee Home",
                        "grid_draw_kw": 22,
                        "installed_at": "2026-04-19",
                        "status": "in_operation"
                      },
                      "section_14a": {
                        "is_controllable": true,
                        "control_responsibility": "grid_operator",
                        "modules": "1",
                        "consent": {
                          "given_by": "end_customer",
                          "given_at": "2026-04-15T09:30:00Z",
                          "channel": "email"
                        }
                      },
                      "connection": {
                        "meter_number": "1ESY1161234567",
                        "has_smart_meter": false
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The case as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Case"
                },
                "examples": {
                  "needs_input": {
                    "$ref": "#/components/examples/CaseNeedsInput"
                  },
                  "ready": {
                    "$ref": "#/components/examples/CaseReady"
                  },
                  "filed": {
                    "$ref": "#/components/examples/CaseFiled"
                  }
                }
              }
            }
          },
          "409": {
            "description": "The idempotency key was reused with a different body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "A declared value was rejected.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "validation_failed": {
                    "$ref": "#/components/examples/ProblemValidation"
                  }
                }
              }
            }
          },
          "402": {
            "description": "No credits left. The case is not created.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Retry-After is set.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/ladeeinrichtung/upload": {
      "post": {
        "operationId": "uploadLadeeinrichtung",
        "summary": "Upload files for a charge point",
        "description": "Opens the case from files, the way a back office has them: the documents, the customer's mail, and whatever fields you already hold. `curl -F file=@rechnung.pdf -F file=@typenschild.jpg -F text=<kundenmail.txt` onto this route answers with the new case and all three under `material[]`, already being read.\n\nOne `file` part per document (a PDF, a JPEG, a PNG, a WebP or a HEIC image, up to 25 MB each), `text` for a pasted mail, and the JSON body's own fields as further parts, `declared` as JSON text. What a file is comes from its bytes, so the part's media type does not matter; a file of any other kind is a 415 naming it, and nothing of the request is stored. Empty parts are ignored, so the form this page's client builds, every row checked, goes through.\n\nThe case that comes back is the same one `POST /v1/anmeldungen/ladeeinrichtung` returns, and everything afterwards is identical: read it, correct it, add to it with `POST /{id}/upload`, file it. A JSON body sent here is a 415 naming the create route; the two doors do not overlap, so neither has a second shape to learn.",
        "tags": [
          "Wallboxes and charge points"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Replaying the same key with the same body returns the original case rather than creating a second one. The same key with a different body is a 409. Stored for 24 hours. Strongly recommended: a duplicate grid registration costs a credit and has to be withdrawn by hand."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "One part per document, repeated for each file: a PDF, a JPEG, a PNG, a WebP or a HEIC image, up to 25 MB each. What a file is comes from its bytes, so the part's own media type does not matter; a file of any other kind is a 415 naming it, and nothing of the request is stored."
                  },
                  "text": {
                    "type": "string",
                    "description": "A pasted mail or a note, read like a file. `curl -F text=<kundenmail.txt` sends one."
                  },
                  "external_ref": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "As in the JSON body."
                  },
                  "installer_ref": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "As in the JSON body."
                  },
                  "intent": {
                    "type": "string",
                    "enum": [
                      "draft",
                      "file"
                    ],
                    "default": "draft",
                    "description": "As in the JSON body."
                  },
                  "operator": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9-]{0,62}$",
                    "description": "As in the JSON body."
                  },
                  "declared": {
                    "type": "string",
                    "description": "The JSON body's `declared` object, as JSON text."
                  }
                }
              },
              "encoding": {
                "file": {
                  "contentType": "application/pdf, image/jpeg, image/png, image/webp, image/heic"
                }
              },
              "examples": {
                "multipart": {
                  "summary": "Multipart: two files and the customer's mail",
                  "description": "Pick the files; the rest of the parts are text. The answer is the case with all of it under `material[]`, already being read.",
                  "value": {
                    "text": "Hallo, anbei Rechnung und Typenschild. Zaehlernummer 1ESY1189004412.",
                    "external_ref": "ERP-9001",
                    "intent": "draft"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The case as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Case"
                },
                "examples": {
                  "needs_input": {
                    "$ref": "#/components/examples/CaseNeedsInput"
                  },
                  "ready": {
                    "$ref": "#/components/examples/CaseReady"
                  },
                  "filed": {
                    "$ref": "#/components/examples/CaseFiled"
                  }
                }
              }
            }
          },
          "409": {
            "description": "The idempotency key was reused with a different body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "A file over 25 MB.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "415": {
            "description": "A file the reader cannot take, named; or a JSON body, which belongs on the create route.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "A part was refused, with its name.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/ladeeinrichtung/fields": {
      "get": {
        "operationId": "getLadeeinrichtungFieldDictionary",
        "summary": "Fields for a charge point",
        "description": "Every field that applies to a charge point, with its field number, the standard's own label and help text, its type and option set, when it is required, and who is expected to know it.\n\nAlready scoped by the path, so there is no `category` parameter to get wrong. Build your intake form from this rather than hardcoding our schema and it keeps working as the dictionary grows.",
        "tags": [
          "Wallboxes and charge points"
        ],
        "parameters": [
          {
            "name": "operator",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"
            },
            "description": "Include that grid operator's own requirements, marked `operator_specific`. Has no effect under a universal key, which by design sees the same field list whichever operator it names; see `requirements` on a case."
          },
          {
            "name": "lang",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "de",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The dictionary.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "version": {
                      "type": "string"
                    },
                    "surface": {
                      "type": "string",
                      "enum": [
                        "universal",
                        "operator"
                      ],
                      "description": "Which surface answered you. Echoed so you need not remember which key you used."
                    },
                    "operator": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  },
                  "required": [
                    "version",
                    "surface",
                    "data"
                  ]
                },
                "examples": {
                  "slice": {
                    "$ref": "#/components/examples/FieldDictionarySlice"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/klimaanlage": {
      "post": {
        "operationId": "createKlimaanlage",
        "summary": "Register an air conditioning unit",
        "description": "**Three bodies to try.** Under *Request Example* on the right, the selector beside *Test Request* switches between a **fixture** the sandbox reads for free, the **raw** shape (the customer's mail and two files, nothing structured) and **declared** fields mapped from your CRM. Whichever is selected there is what *Test Request* opens with; inside the client the same bodies sit in its sidebar, under this request. Files of your own go to *Upload files*, the operation right after this one, which takes them as a form with a file picker.\n\nOpens a case for an installation used for Raumkühlung, and with it the § 14a registration as a steuerbare Verbrauchseinrichtung where the installed capacity behind the connection passes 4.2 kW.\n\n**The path pins `device.category` to `air_conditioning`.** Leave the field out; sending a different value is a 400 rather than a silent overwrite.\n\n88 fields in scope, 58 mandatory. The type block is fields 7001 to 7017, and it is the fourth and last § 14a device. Note that the standard does NOT make an air conditioner's manufacturer and type mandatory the way it does a charge point's, so neither blocks here.\n\n`section_14a.critical_infrastructure` (field 7006) does block. An installation serving betriebsnotwendige Zwecke or critical infrastructure is not one the operator may throttle, and filing without answering it would consent on the customer's behalf to control they are entitled to refuse.\n\nWhat it creates is an ordinary Anmeldung, read and filed on `/v1/anmeldungen/{id}` like any other.",
        "tags": [
          "Air conditioning"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Replaying the same key with the same body returns the original case rather than creating a second one. The same key with a different body is a 409. Stored for 24 hours. Strongly recommended: a duplicate grid registration costs a credit and has to be withdrawn by hand."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateRequest"
              },
              "examples": {
                "fixture": {
                  "summary": "Fixture: a hosted air conditioning invoice, free, no model call",
                  "value": {
                    "external_ref": "ERP-5501",
                    "intent": "draft",
                    "material": [
                      {
                        "fixture": "air-conditioning-invoice-de"
                      }
                    ]
                  }
                },
                "raw": {
                  "summary": "Raw: the customer's mail and two files, nothing structured",
                  "description": "The shape a back office actually has: a pasted mail and the attachments, no fields. Every value comes back `extracted`, each with the quote it was read from. The two `data` values here are a PDF header and a JPEG header and nothing after: valid base64, sendable, far too little to be documents, so they read as unreadable and the mail carries the case. Send real files and they read for real, 25 a day on the sandbox.",
                  "value": {
                    "external_ref": "ERP-5503",
                    "intent": "draft",
                    "material": [
                      {
                        "kind": "text",
                        "label": "Kundenmail",
                        "content": "Hallo,\n\nanbei Rechnung und Typenschild zu unserer neuen Klimaanlage (Daikin Perfera FTXM50R, 1,6 kW, Split). Zaehlernummer 1ESY1130077890. Keine kritische Infrastruktur, bitte Modul 1 nach 14a EnWG anmelden.\n\nViele Gruesse\nMaria Brandt\nAm Winterhafen 12, 55131 Mainz"
                      },
                      {
                        "kind": "file",
                        "filename": "rechnung-klimatechnik.pdf",
                        "content_type": "application/pdf",
                        "data": "JVBERi0xLjQK"
                      },
                      {
                        "kind": "file",
                        "filename": "typenschild-klimageraet.jpg",
                        "content_type": "image/jpeg",
                        "data": "/9j/4AAQSkZJRgAB"
                      }
                    ]
                  }
                },
                "declared": {
                  "summary": "Declared: fields mapped from your CRM, 1.6 kW, not critical infrastructure",
                  "value": {
                    "external_ref": "ERP-5502",
                    "intent": "draft",
                    "declared": {
                      "site": {
                        "street": "Am Winterhafen",
                        "house_number": "12",
                        "postal_code": "55131",
                        "city": "Mainz"
                      },
                      "end_customer": {
                        "first_name": "Maria",
                        "last_name": "Brandt",
                        "email": "m.brandt@example.de",
                        "phone": "+49 6131 5550166",
                        "is_owner": true
                      },
                      "installer": {
                        "company": "Klimatechnik Rhein-Main GmbH",
                        "email": "service@klimatechnik-rhein-main.example"
                      },
                      "device": {
                        "count": 1,
                        "manufacturer": "Daikin",
                        "model": "Perfera FTXM50R",
                        "electrical_power_kw": 1.6,
                        "starting_current_a": 8.5,
                        "grid_draw_kw": 1.6,
                        "installed_at": "2026-08-05",
                        "status": "planned"
                      },
                      "section_14a": {
                        "is_controllable": true,
                        "critical_infrastructure": false,
                        "control_responsibility": "metering_operator",
                        "modules": "1",
                        "consent": {
                          "given_by": "end_customer",
                          "given_at": "2026-08-01T09:30:00Z",
                          "channel": "email"
                        }
                      },
                      "connection": {
                        "meter_number": "1ESY1130077890"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The case as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Case"
                },
                "examples": {
                  "needs_input": {
                    "$ref": "#/components/examples/CaseNeedsInput"
                  },
                  "ready": {
                    "$ref": "#/components/examples/CaseReady"
                  },
                  "filed": {
                    "$ref": "#/components/examples/CaseFiled"
                  }
                }
              }
            }
          },
          "409": {
            "description": "The idempotency key was reused with a different body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "A declared value was rejected.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "validation_failed": {
                    "$ref": "#/components/examples/ProblemValidation"
                  }
                }
              }
            }
          },
          "402": {
            "description": "No credits left. The case is not created.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Retry-After is set.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/klimaanlage/upload": {
      "post": {
        "operationId": "uploadKlimaanlage",
        "summary": "Upload files for an air conditioning unit",
        "description": "Opens the case from files, the way a back office has them: the documents, the customer's mail, and whatever fields you already hold. `curl -F file=@rechnung.pdf -F file=@typenschild.jpg -F text=<kundenmail.txt` onto this route answers with the new case and all three under `material[]`, already being read.\n\nOne `file` part per document (a PDF, a JPEG, a PNG, a WebP or a HEIC image, up to 25 MB each), `text` for a pasted mail, and the JSON body's own fields as further parts, `declared` as JSON text. What a file is comes from its bytes, so the part's media type does not matter; a file of any other kind is a 415 naming it, and nothing of the request is stored. Empty parts are ignored, so the form this page's client builds, every row checked, goes through.\n\nThe case that comes back is the same one `POST /v1/anmeldungen/klimaanlage` returns, and everything afterwards is identical: read it, correct it, add to it with `POST /{id}/upload`, file it. A JSON body sent here is a 415 naming the create route; the two doors do not overlap, so neither has a second shape to learn.",
        "tags": [
          "Air conditioning"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Replaying the same key with the same body returns the original case rather than creating a second one. The same key with a different body is a 409. Stored for 24 hours. Strongly recommended: a duplicate grid registration costs a credit and has to be withdrawn by hand."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "One part per document, repeated for each file: a PDF, a JPEG, a PNG, a WebP or a HEIC image, up to 25 MB each. What a file is comes from its bytes, so the part's own media type does not matter; a file of any other kind is a 415 naming it, and nothing of the request is stored."
                  },
                  "text": {
                    "type": "string",
                    "description": "A pasted mail or a note, read like a file. `curl -F text=<kundenmail.txt` sends one."
                  },
                  "external_ref": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "As in the JSON body."
                  },
                  "installer_ref": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "As in the JSON body."
                  },
                  "intent": {
                    "type": "string",
                    "enum": [
                      "draft",
                      "file"
                    ],
                    "default": "draft",
                    "description": "As in the JSON body."
                  },
                  "operator": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9-]{0,62}$",
                    "description": "As in the JSON body."
                  },
                  "declared": {
                    "type": "string",
                    "description": "The JSON body's `declared` object, as JSON text."
                  }
                }
              },
              "encoding": {
                "file": {
                  "contentType": "application/pdf, image/jpeg, image/png, image/webp, image/heic"
                }
              },
              "examples": {
                "multipart": {
                  "summary": "Multipart: two files and the customer's mail",
                  "description": "Pick the files; the rest of the parts are text. The answer is the case with all of it under `material[]`, already being read.",
                  "value": {
                    "text": "Hallo, anbei Rechnung und Typenschild. Zaehlernummer 1ESY1189004412.",
                    "external_ref": "ERP-9001",
                    "intent": "draft"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The case as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Case"
                },
                "examples": {
                  "needs_input": {
                    "$ref": "#/components/examples/CaseNeedsInput"
                  },
                  "ready": {
                    "$ref": "#/components/examples/CaseReady"
                  },
                  "filed": {
                    "$ref": "#/components/examples/CaseFiled"
                  }
                }
              }
            }
          },
          "409": {
            "description": "The idempotency key was reused with a different body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "A file over 25 MB.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "415": {
            "description": "A file the reader cannot take, named; or a JSON body, which belongs on the create route.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "A part was refused, with its name.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/klimaanlage/fields": {
      "get": {
        "operationId": "getKlimaanlageFieldDictionary",
        "summary": "Fields for an air conditioning unit",
        "description": "Every field that applies to an air conditioning unit, with its field number, the standard's own label and help text, its type and option set, when it is required, and who is expected to know it.\n\nAlready scoped by the path, so there is no `category` parameter to get wrong.",
        "tags": [
          "Air conditioning"
        ],
        "parameters": [
          {
            "name": "operator",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"
            },
            "description": "Include that grid operator's own requirements, marked `operator_specific`. Has no effect under a universal key, which by design sees the same field list whichever operator it names; see `requirements` on a case."
          },
          {
            "name": "lang",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "de",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The dictionary.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "version": {
                      "type": "string"
                    },
                    "surface": {
                      "type": "string",
                      "enum": [
                        "universal",
                        "operator"
                      ],
                      "description": "Which surface answered you. Echoed so you need not remember which key you used."
                    },
                    "operator": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  },
                  "required": [
                    "version",
                    "surface",
                    "data"
                  ]
                },
                "examples": {
                  "slice": {
                    "$ref": "#/components/examples/FieldDictionarySlice"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/14a": {
      "post": {
        "operationId": "create14a",
        "summary": "Create a § 14a registration",
        "description": "**Four bodies to try.** Under *Request Example* on the right, the selector beside *Test Request* switches between a **fixture** the sandbox reads for free, the **raw** shape (the customer's mail and two files, nothing structured), **declared** fields mapped from your CRM, and **mixed**, some of each. Whichever is selected there is what *Test Request* opens with; inside the client the same bodies sit in its sidebar, under this request. Files of your own go to *Upload files*, the operation right after this one, which takes them as a form with a file picker.\n\nCreates a case from declared fields, raw material, or both. Extraction runs immediately for inline material and the response already reflects it for small inputs; a case whose material is still being read comes back as `extracting` and should be polled or awaited by webhook.\n\nThis is the only § 14a-specific path. What it creates is an ordinary Anmeldung: read it, correct it, add material to it and file it through `/v1/anmeldungen/{id}` exactly as you would a case created any other way.",
        "tags": [
          "§ 14a, any device"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Replaying the same key with the same body returns the original case rather than creating a second one. The same key with a different body is a 409. Stored for 24 hours. Strongly recommended: a duplicate grid registration costs a credit and has to be withdrawn by hand."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateRequest"
              },
              "examples": {
                "fixture": {
                  "summary": "Fixture: a hosted invoice, free, no model call",
                  "description": "Send this one first. `fixture` names a document we host and have already read, so nothing is uploaded and no model call is charged for reading it, and a screen under construction can rebuild against it all day.\n\nThe result is a real case in the sandbox plane, with the same fields, the same quotes and the same shape a live call returns. Nothing it does can file, charge or email anybody.",
                  "value": {
                    "external_ref": "ERP-4711",
                    "intent": "draft",
                    "material": [
                      {
                        "fixture": "wallbox-invoice-de"
                      }
                    ]
                  }
                },
                "raw": {
                  "summary": "Raw: drop everything, declare nothing",
                  "description": "The whole point of the raw channel: an installer's back office has a customer mail and three PDFs and nothing structured at all. Every field in the response is then `source: \"extracted\"`, each carrying the quote it was read from.\n\nWorks on both planes. Reading a document you supply costs a model call per file, which is why the sandbox reads at most 25 real documents per account per day; fixtures are unlimited there.\n\nThe two `data` values are a PDF header and nothing after it: valid base64, so the request is sendable, and far too little to be a document. They carried a trailing `…` until 2026-09-06, which made this example fail validation on encoding the moment anybody pressed Send on it.",
                  "value": {
                    "external_ref": "ERP-4711",
                    "intent": "draft",
                    "material": [
                      {
                        "kind": "text",
                        "label": "Kundenmail",
                        "content": "Hallo, anbei die Unterlagen zu meiner Wallbox. Die Anlage laeuft seit 2022. Ich bin Eigentuemer. Bitte Modul 1 nach 14a EnWG anmelden.\n\nViele Gruesse\nAlexander Bergmann"
                      },
                      {
                        "kind": "file",
                        "filename": "technische-zustimmung.pdf",
                        "content_type": "application/pdf",
                        "data": "JVBERi0xLjQKJeLjz9MK"
                      },
                      {
                        "kind": "file",
                        "filename": "schlussrechnung.pdf",
                        "content_type": "application/pdf",
                        "data": "JVBERi0xLjQKJcfsj6IK"
                      }
                    ]
                  }
                },
                "declared": {
                  "summary": "Declared: declare everything, drop nothing",
                  "description": "An ERP that already holds the data. No extraction runs, the case is `ready` on the first call, and `intent: \"file\"` lodges it in one round trip.",
                  "value": {
                    "external_ref": "ERP-4712",
                    "intent": "file",
                    "declared": {
                      "site": {
                        "street": "Ahornweg",
                        "house_number": "7a",
                        "postal_code": "22607",
                        "city": "Hamburg"
                      },
                      "end_customer": {
                        "first_name": "Alexander",
                        "last_name": "Bergmann",
                        "email": "a.bergmann@example.de",
                        "phone": "+49 40 5550123",
                        "is_owner": true
                      },
                      "installer": {
                        "company": "Nordlicht Elektrotechnik GmbH",
                        "contact_first_name": "Jan",
                        "contact_last_name": "Petersen",
                        "email": "petersen@example.de",
                        "registry_number": "HH-EL-04127"
                      },
                      "device": {
                        "category": "charge_point",
                        "manufacturer": "ABL",
                        "model": "eMH3 3W2250",
                        "count": 1,
                        "grid_draw_kw": 11,
                        "installed_at": "2022-04-19",
                        "status": "in_operation"
                      },
                      "connection": {
                        "meter_number": "1ISK0000123456",
                        "has_smart_meter": true
                      },
                      "section_14a": {
                        "modules": "1",
                        "control_responsibility": "metering_operator",
                        "consent": {
                          "given_by": "end_customer",
                          "given_at": "2026-08-17T14:05:00Z",
                          "channel": "phone",
                          "reference": "call-2026-08-17-1405"
                        }
                      }
                    }
                  }
                },
                "mixed": {
                  "summary": "Mixed: some fields declared, the rest in a PDF",
                  "description": "Some fields are known, the rest is in a PDF. Declared values are never overwritten by extraction; where extraction disagrees, the disagreement is reported in `conflicts` instead of being silently resolved.",
                  "value": {
                    "external_ref": "ERP-4713",
                    "intent": "draft",
                    "declared": {
                      "site": {
                        "street": "Ahornweg",
                        "house_number": "7a",
                        "postal_code": "22607",
                        "city": "Hamburg"
                      },
                      "end_customer": {
                        "first_name": "Alexander",
                        "last_name": "Bergmann",
                        "email": "a.bergmann@example.de"
                      },
                      "device": {
                        "category": "charge_point"
                      },
                      "section_14a": {
                        "modules": "1",
                        "consent": {
                          "given_by": "end_customer",
                          "given_at": "2026-08-17T14:05:00Z",
                          "channel": "phone",
                          "reference": "call-2026-08-17-1405"
                        }
                      },
                      "nr": {
                        "5017": "11"
                      }
                    },
                    "material": [
                      {
                        "kind": "file",
                        "storage_id": "kg21x9…",
                        "filename": "schlussrechnung.pdf",
                        "content_type": "application/pdf"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The case as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Case"
                },
                "examples": {
                  "needs_input": {
                    "$ref": "#/components/examples/CaseNeedsInput"
                  },
                  "ready": {
                    "$ref": "#/components/examples/CaseReady"
                  },
                  "filed": {
                    "$ref": "#/components/examples/CaseFiled"
                  }
                }
              }
            }
          },
          "409": {
            "description": "The idempotency key was reused with a different body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "A declared value was rejected.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "validation_failed": {
                    "$ref": "#/components/examples/ProblemValidation"
                  }
                }
              }
            }
          },
          "402": {
            "description": "No credits left. The case is not created.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Retry-After is set.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/14a/upload": {
      "post": {
        "operationId": "upload14a",
        "summary": "Upload files for a § 14a registration",
        "description": "Opens the case from files, the way a back office has them: the documents, the customer's mail, and whatever fields you already hold. `curl -F file=@rechnung.pdf -F file=@typenschild.jpg -F text=<kundenmail.txt` onto this route answers with the new case and all three under `material[]`, already being read.\n\nOne `file` part per document (a PDF, a JPEG, a PNG, a WebP or a HEIC image, up to 25 MB each), `text` for a pasted mail, and the JSON body's own fields as further parts, `declared` as JSON text. What a file is comes from its bytes, so the part's media type does not matter; a file of any other kind is a 415 naming it, and nothing of the request is stored. Empty parts are ignored, so the form this page's client builds, every row checked, goes through.\n\nThe case that comes back is the same one `POST /v1/anmeldungen/14a` returns, and everything afterwards is identical: read it, correct it, add to it with `POST /{id}/upload`, file it. A JSON body sent here is a 415 naming the create route; the two doors do not overlap, so neither has a second shape to learn.",
        "tags": [
          "§ 14a, any device"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Replaying the same key with the same body returns the original case rather than creating a second one. The same key with a different body is a 409. Stored for 24 hours. Strongly recommended: a duplicate grid registration costs a credit and has to be withdrawn by hand."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "One part per document, repeated for each file: a PDF, a JPEG, a PNG, a WebP or a HEIC image, up to 25 MB each. What a file is comes from its bytes, so the part's own media type does not matter; a file of any other kind is a 415 naming it, and nothing of the request is stored."
                  },
                  "text": {
                    "type": "string",
                    "description": "A pasted mail or a note, read like a file. `curl -F text=<kundenmail.txt` sends one."
                  },
                  "external_ref": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "As in the JSON body."
                  },
                  "installer_ref": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "As in the JSON body."
                  },
                  "intent": {
                    "type": "string",
                    "enum": [
                      "draft",
                      "file"
                    ],
                    "default": "draft",
                    "description": "As in the JSON body."
                  },
                  "operator": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9-]{0,62}$",
                    "description": "As in the JSON body."
                  },
                  "declared": {
                    "type": "string",
                    "description": "The JSON body's `declared` object, as JSON text."
                  }
                }
              },
              "encoding": {
                "file": {
                  "contentType": "application/pdf, image/jpeg, image/png, image/webp, image/heic"
                }
              },
              "examples": {
                "multipart": {
                  "summary": "Multipart: two files and the customer's mail",
                  "description": "Pick the files; the rest of the parts are text. The answer is the case with all of it under `material[]`, already being read.",
                  "value": {
                    "text": "Hallo, anbei Rechnung und Typenschild. Zaehlernummer 1ESY1189004412.",
                    "external_ref": "ERP-9001",
                    "intent": "draft"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The case as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Case"
                },
                "examples": {
                  "needs_input": {
                    "$ref": "#/components/examples/CaseNeedsInput"
                  },
                  "ready": {
                    "$ref": "#/components/examples/CaseReady"
                  },
                  "filed": {
                    "$ref": "#/components/examples/CaseFiled"
                  }
                }
              }
            }
          },
          "409": {
            "description": "The idempotency key was reused with a different body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "A file over 25 MB.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "415": {
            "description": "A file the reader cannot take, named; or a JSON body, which belongs on the create route.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "A part was refused, with its name.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/14a/fields": {
      "get": {
        "operationId": "get14aFieldDictionary",
        "summary": "The field dictionary",
        "description": "Every field this endpoint understands, with its field number, the standard's own label and help text, its type and option set, when it is required, and who is expected to know it. Build your intake form from this rather than hardcoding our schema, and it keeps working as the dictionary grows.",
        "tags": [
          "§ 14a, any device"
        ],
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "charge_point",
                "heat_pump"
              ]
            },
            "description": "Restrict to the fields that apply to one device family."
          },
          {
            "name": "operator",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"
            },
            "description": "Include that grid operator's own requirements, marked `operator_specific`. Has no effect under a universal key, which by design sees the same field list whichever operator it names; see `requirements` on a case."
          },
          {
            "name": "lang",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "de",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The dictionary.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "version": {
                      "type": "string"
                    },
                    "surface": {
                      "type": "string",
                      "enum": [
                        "universal",
                        "operator"
                      ],
                      "description": "Which surface answered you. Echoed so you need not remember which key you used."
                    },
                    "operator": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  },
                  "required": [
                    "version",
                    "surface",
                    "data"
                  ]
                },
                "examples": {
                  "slice": {
                    "$ref": "#/components/examples/FieldDictionarySlice"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen": {
      "get": {
        "operationId": "listAnmeldungen",
        "summary": "List your cases",
        "description": "Summary rows, newest first, so a nightly reconciliation stays cheap. A case's detail (what is resolved, open or contradicted) is on the case itself, because returning it for every row would make the common call the expensive one.\n\nOne page holds one mode. A live key sees live cases and a sandbox key sees sandbox ones; there is no parameter to mix them, because a page that did would put test data into a production screen.",
        "tags": [
          "The case"
        ],
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "From `next_cursor` of a previous page."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of cases.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "mode": {
                      "type": "string",
                      "enum": [
                        "live",
                        "test"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "external_ref": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "status": {
                            "type": "string"
                          },
                          "label": {
                            "type": "string"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "anm_7Kq3ZmR8vT",
                      "external_ref": "ERP-4711",
                      "status": "needs_input",
                      "label": "Alexander Bergmann, Ahornweg 7a",
                      "created_at": "2026-08-30T09:12:04Z",
                      "updated_at": "2026-08-30T09:12:41Z"
                    }
                  ],
                  "next_cursor": null
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/{id}": {
      "get": {
        "operationId": "getAnmeldung",
        "summary": "Read a case",
        "description": "Current state. For the history, use the events endpoint.",
        "tags": [
          "The case"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The case id, e.g. `anm_7Kq3ZmR8vT`. Ids always carry the `anm_` prefix, which is what keeps the static `14a` segment on the create path unambiguous against this parameter."
          }
        ],
        "responses": {
          "200": {
            "description": "The case as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Case"
                },
                "examples": {
                  "needs_input": {
                    "$ref": "#/components/examples/CaseNeedsInput"
                  },
                  "ready": {
                    "$ref": "#/components/examples/CaseReady"
                  },
                  "filed": {
                    "$ref": "#/components/examples/CaseFiled"
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such case.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateAnmeldung",
        "summary": "Declare or correct values",
        "description": "The granular channel after creation. Anything declared here wins over anything extracted, which is also how a conflict is resolved.",
        "tags": [
          "The case"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The case id, e.g. `anm_7Kq3ZmR8vT`. Ids always carry the `anm_` prefix, which is what keeps the static `14a` segment on the create path unambiguous against this parameter."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchRequest"
              },
              "examples": {
                "resolve_conflict": {
                  "$ref": "#/components/examples/PatchResolveConflict"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The case as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Case"
                },
                "examples": {
                  "needs_input": {
                    "$ref": "#/components/examples/CaseNeedsInput"
                  },
                  "ready": {
                    "$ref": "#/components/examples/CaseReady"
                  },
                  "filed": {
                    "$ref": "#/components/examples/CaseFiled"
                  }
                }
              }
            }
          },
          "409": {
            "description": "The case is already filed and can no longer be edited.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "A declared value was rejected.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "discardAnmeldung",
        "summary": "Discard a draft",
        "description": "Only a case that has not been filed. Filed cases are withdrawn, not deleted.",
        "tags": [
          "The case"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The case id, e.g. `anm_7Kq3ZmR8vT`. Ids always carry the `anm_` prefix, which is what keeps the static `14a` segment on the create path unambiguous against this parameter."
          }
        ],
        "responses": {
          "204": {
            "description": "Discarded."
          },
          "409": {
            "description": "Already filed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/{id}/material": {
      "post": {
        "operationId": "addMaterial",
        "summary": "Add raw material",
        "description": "Appends to an existing case and starts reading it. Values already declared are untouched; new extractions may open or close fields and may raise conflicts.\n\nFiles of your own go to `POST /{id}/upload` as a form, the way `curl -F file=@nachtrag.pdf` sends them. Here, in JSON, a file travels inline as base64 `data`, up to 8 MB, or as the `storage_id` an upload target handed back after `POST /{id}/documents`. See the overview, Sending a document.\n\nAllowed on any unfiled case whatever its `status`: `needs_input`, `extracting` or `ready`. The new material is read the same way and the case is resolved again, so a `ready` case may go back to `needs_input` if the new document contradicts something. A filed case answers 409.",
        "tags": [
          "The case"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The case id, e.g. `anm_7Kq3ZmR8vT`. Ids always carry the `anm_` prefix, which is what keeps the static `14a` segment on the create path unambiguous against this parameter."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "material": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/Material"
                    },
                    "minItems": 1,
                    "maxItems": 30
                  }
                },
                "required": [
                  "material"
                ]
              },
              "examples": {
                "uploaded": {
                  "summary": "Uploaded: the storage id an upload target answered with",
                  "value": {
                    "material": [
                      {
                        "kind": "file",
                        "storage_id": "kg21x9q8v0s5c7b2m4n6p8r0t2w4y6a",
                        "filename": "rechnung.pdf",
                        "content_type": "application/pdf"
                      }
                    ]
                  }
                },
                "inline": {
                  "summary": "Inline: a small file as base64 in `data`",
                  "description": "The `data` here is a PDF header and nothing after it: valid base64, far too little to be a document.",
                  "value": {
                    "material": [
                      {
                        "kind": "file",
                        "filename": "typenschild.pdf",
                        "content_type": "application/pdf",
                        "data": "JVBERi0xLjQK"
                      }
                    ]
                  }
                },
                "text": {
                  "summary": "Text: a pasted customer mail",
                  "value": {
                    "material": [
                      {
                        "kind": "text",
                        "label": "Kundenmail",
                        "content": "Hallo, anbei die Unterlagen zur PV-Anlage. Die Module sind seit Juni montiert.\n\nViele Gruesse\nInes Kaufmann"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The case as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Case"
                },
                "examples": {
                  "needs_input": {
                    "$ref": "#/components/examples/CaseNeedsInput"
                  },
                  "ready": {
                    "$ref": "#/components/examples/CaseReady"
                  },
                  "filed": {
                    "$ref": "#/components/examples/CaseFiled"
                  }
                }
              }
            }
          },
          "413": {
            "description": "Material too large.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/{id}/upload": {
      "post": {
        "operationId": "uploadToAnmeldung",
        "summary": "Upload files to a case",
        "description": "Adds files to a case that exists and starts reading them. `curl -F file=@nachtrag.pdf` onto this route is the whole call; `text` carries a pasted mail. Up to 25 MB per file; what a file is comes from its bytes. Values already declared are untouched; new extractions may open or close fields and may raise conflicts.\n\nAllowed on any unfiled case whatever its `status`, `ready` included; the case is resolved again once the new material is read. A filed case answers 409.\n\nThe JSON way in is `POST /{id}/material`, for a file inline as base64 or by storage id; a JSON body sent here is a 415 naming it.",
        "tags": [
          "The case"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The case id, e.g. `anm_7Kq3ZmR8vT`. Ids always carry the `anm_` prefix, which is what keeps the static `14a` segment on the create path unambiguous against this parameter."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "One part per document, repeated for each file: a PDF, a JPEG, a PNG, a WebP or a HEIC image, up to 25 MB each. What a file is comes from its bytes, so the part's own media type does not matter; a file of any other kind is a 415 naming it, and nothing of the request is stored."
                  },
                  "text": {
                    "type": "string",
                    "description": "A pasted mail or a note, read like a file. `curl -F text=<kundenmail.txt` sends one."
                  }
                }
              },
              "encoding": {
                "file": {
                  "contentType": "application/pdf, image/jpeg, image/png, image/webp, image/heic"
                }
              },
              "examples": {
                "multipart": {
                  "summary": "Multipart: a file added to the case",
                  "description": "Pick the file. `curl -F file=@nachtrag.pdf` onto this route does the same.",
                  "value": {}
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The case as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Case"
                },
                "examples": {
                  "needs_input": {
                    "$ref": "#/components/examples/CaseNeedsInput"
                  },
                  "ready": {
                    "$ref": "#/components/examples/CaseReady"
                  },
                  "filed": {
                    "$ref": "#/components/examples/CaseFiled"
                  }
                }
              }
            }
          },
          "413": {
            "description": "A file over 25 MB.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "415": {
            "description": "A file the reader cannot take, named; or a JSON body, which belongs on /material.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/{id}/documents": {
      "post": {
        "operationId": "createUploadTarget",
        "summary": "Get an upload target",
        "description": "For a client that streams bytes and never holds the file. Everyone else sends the file itself, as a form, to `POST /v1/anmeldungen/{typ}/upload` or `POST /{id}/upload`, in one call.\n\nThree calls. Ask for a target here, POST the raw bytes to `upload_url` with the file's own `Content-Type` and no bearer (the target is the credential, single-use, expiring within the hour), and the storage answers `{\"storageId\": \"...\"}`. Send that as `storage_id` on `POST /{id}/material`, and the case reads it. The SDK's `cases.uploadByTarget` makes the three calls for you.\n\nOnly the target comes back. An earlier draft of this contract also returned a `document_id`, which would have meant writing a document row before the bytes existed: no storage key, no media type, no size: precisely the fields that make case storage safe to read from. The id a caller needs is the one the upload itself hands them.\n\nOwnership is checked before a target is issued. An upload URL for someone else's case would be a way to attach documents to it.",
        "tags": [
          "The case"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The case id, e.g. `anm_7Kq3ZmR8vT`. Ids always carry the `anm_` prefix, which is what keeps the static `14a` segment on the create path unambiguous against this parameter."
          }
        ],
        "responses": {
          "201": {
            "description": "A short-lived upload target.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "upload_url": {
                      "type": "string",
                      "format": "uri"
                    }
                  },
                  "required": [
                    "upload_url"
                  ]
                },
                "example": {
                  "upload_url": "https://…/api/storage/upload?token=…"
                }
              }
            }
          },
          "404": {
            "description": "No such case, or not yours.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "The case is already filed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/{id}/file": {
      "post": {
        "operationId": "fileAnmeldung",
        "summary": "File the registration",
        "description": "Commits the case and charges the credit. If anything blocking is open or contradicted the case is returned unchanged as `needs_input` with a 409 and no credit is charged.\n\n**It does not sign, and no field here can make it.** Two signatures on the § 14a form are never ours. Filing produces the document and emails the end customer asking them to sign it, from the installer's own verified domain where there is one, replying to a mailbox we read so the signed form comes back to the case. Watch `signature` on the case for where that stands.\n\nThe routing is best effort: a case with no filled form yet, no customer address, or no operated mailbox is still filed, and `signature` stays null until somebody completes it. Refusing the filing over it would make this call fail for reasons that have nothing to do with filing.",
        "tags": [
          "The case"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The case id, e.g. `anm_7Kq3ZmR8vT`. Ids always carry the `anm_` prefix, which is what keeps the static `14a` segment on the create path unambiguous against this parameter."
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Replaying the same key with the same body returns the original case rather than creating a second one. The same key with a different body is a 409. Stored for 24 hours. Strongly recommended: a duplicate grid registration costs a credit and has to be withdrawn by hand."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "sandbox_outcome": {
                    "type": "string",
                    "enum": [
                      "accepted",
                      "rejected",
                      "needs_more_information"
                    ],
                    "default": "accepted",
                    "description": "SANDBOX ONLY. What the grid operator decides. If the sandbox always accepted, nobody would build the rejection path, so you choose, and the case lands in the same stage a live one will, with a stated reason.\n\nUnder a live key this is a 422 naming the field, not an ignore. A live filing's outcome comes from the operator."
                  },
                  "sandbox_pace": {
                    "type": "string",
                    "enum": [
                      "instant",
                      "staged"
                    ],
                    "default": "instant",
                    "description": "SANDBOX ONLY. How fast the sandbox operator answers.\n\n`instant` answers inside the filing call, which is what a test suite wants. `staged` is the one thing an instant answer cannot show: a live case is filed, waits, and is answered later. Staged walks `submitted`, `waiting_operator` and then `sandbox_outcome` a few seconds apart on a timer, each step a `case.status_changed` event your webhook receives in turn, so a receiver that has only ever seen the answer arrive inside the filing call gets to test the wait.\n\nUnder a live key this is a 422 naming the field. A live filing is answered when the grid operator answers."
                  },
                  "declarations": {
                    "type": "string",
                    "enum": [
                      "release",
                      "withhold"
                    ],
                    "default": "release",
                    "description": "What this hand-over does with the grid operator's standard declarations: the confirmation that the operator's technical connection conditions are met, and the declaration regarding the operator's privacy notice.\n\n`release`, the default, releases them with the hand-over where the account has accepted the terms in the edition of 28 September 2026 or later, whose § 3 (3) provides for it, and does nothing under an earlier edition. `withhold` keeps them open for this case, for one you cannot make them for yet; they are then asked for like any other open question. No other consent an operator asks for, such as a power of attorney or a landowner's permission, is ever set this way."
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The case as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Case"
                },
                "examples": {
                  "needs_input": {
                    "$ref": "#/components/examples/CaseNeedsInput"
                  },
                  "ready": {
                    "$ref": "#/components/examples/CaseReady"
                  },
                  "filed": {
                    "$ref": "#/components/examples/CaseFiled"
                  }
                }
              }
            }
          },
          "402": {
            "description": "No credits left. Never in the sandbox: a sandbox case consumes nothing.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Blocking fields are open, or the case is already filed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`sandbox_outcome` or `sandbox_pace` under a live key, or a value nobody defined.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/{id}/events": {
      "get": {
        "operationId": "listEvents",
        "summary": "Replay the log",
        "description": "Everything that has happened to this case, oldest first: it was created, a document finished being read, values were declared, it was filed, its status moved.\n\n**Poll `case.extraction_completed`.** Reading a document is the slow step, seconds for a note and about a minute for a one-page PDF, more for a scan or a long one, and it is the step after which the case is worth re-reading. Everything else on the case can be had from `GET /v1/anmeldungen/{id}` directly.\n\nHold the `id` of the last event you saw and pass it as `after`; an empty `data` means nothing has happened since. A cursor rather than a timestamp, because two events written in one mutation share a millisecond and a time-based cursor would either repeat one or drop one.",
        "tags": [
          "The case"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The case id, e.g. `anm_7Kq3ZmR8vT`. Ids always carry the `anm_` prefix, which is what keeps the static `14a` segment on the create path unambiguous against this parameter."
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "An event `id` from an earlier page. Everything after it, exclusive. An id belonging to another case is refused."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 100
            },
            "description": "Events per page."
          }
        ],
        "responses": {
          "200": {
            "description": "Events, oldest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "at",
                          "type"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Pass back as `after` to page."
                          },
                          "at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "case.created",
                              "case.material_read",
                              "case.extraction_completed",
                              "case.updated",
                              "case.status_changed",
                              "case.filed",
                              "case.information_requested",
                              "case.information_response_received",
                              "case.information_response_ready",
                              "case.information_response_submitted",
                              "case.operator_login_missing"
                            ]
                          },
                          "status": {
                            "type": "string",
                            "description": "Present on `case.status_changed` and `case.filed`."
                          },
                          "note": {
                            "type": "string",
                            "description": "Customer-safe wording, where the status change has one."
                          },
                          "material": {
                            "type": "string",
                            "description": "Which document. `case.material_read` only."
                          },
                          "material_status": {
                            "type": "string",
                            "enum": [
                              "read",
                              "failed"
                            ]
                          },
                          "extracted_chars": {
                            "type": "integer"
                          },
                          "information_request": {
                            "description": "Present on `case.information_requested`: the immutable snapshot also delivered in the webhook.",
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/InformationRequestSnapshot"
                              }
                            ]
                          },
                          "information_response_id": {
                            "type": "string",
                            "description": "Present on information-response lifecycle events."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/{id}/information-requests/{requestId}": {
      "get": {
        "operationId": "getInformationRequest",
        "summary": "Read an operator information request",
        "description": "The canonical, current request after a `case.information_requested` webhook. The webhook includes a compact immutable `missing` snapshot so an integration can notify its end customer immediately; re-read this resource before presenting a form or accepting a response. The request's `sequence` is append-only: another operator query creates another request rather than changing the first one's meaning.",
        "tags": [
          "The case"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The case id, e.g. `anm_7Kq3ZmR8vT`. Ids always carry the `anm_` prefix, which is what keeps the static `14a` segment on the create path unambiguous against this parameter."
          },
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The `information_request.id` from the webhook, on this key's plane."
          }
        ],
        "responses": {
          "200": {
            "description": "The requested missing fields and documents.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InformationRequest"
                }
              }
            }
          },
          "404": {
            "description": "No such request on this case and plane.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/{id}/information-requests/{requestId}/responses": {
      "post": {
        "operationId": "createInformationResponse",
        "summary": "Provide the requested information",
        "description": "Creates one immutable response bundle. Use an `Idempotency-Key`: a retry with the same body returns the same response, while a different body is refused. Documents are read before the response becomes `ready_to_submit`; wait for `case.information_response_ready`, then call `/submit`.",
        "tags": [
          "The case"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The case id, e.g. `anm_7Kq3ZmR8vT`. Ids always carry the `anm_` prefix, which is what keeps the static `14a` segment on the create path unambiguous against this parameter."
          },
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The `information_request.id` from the webhook, on this key's plane."
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Replaying the same key with the same body returns the original case rather than creating a second one. The same key with a different body is a 409. Stored for 24 hours. Strongly recommended: a duplicate grid registration costs a credit and has to be withdrawn by hand."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InformationResponseRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The immutable response bundle.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InformationResponse"
                }
              }
            }
          },
          "409": {
            "description": "The idempotency key was reused with a different body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "The request is closed, incomplete, or contains fields it did not ask for.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/{id}/information-requests/{requestId}/responses/upload": {
      "post": {
        "operationId": "uploadInformationResponse",
        "summary": "Provide requested documents",
        "description": "The file form equivalent of POST /responses. Send `file` parts, optional `text`, and `declared` as JSON text. It has the same idempotency and processing lifecycle.",
        "tags": [
          "The case"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The case id, e.g. `anm_7Kq3ZmR8vT`. Ids always carry the `anm_` prefix, which is what keeps the static `14a` segment on the create path unambiguous against this parameter."
          },
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The `information_request.id` from the webhook, on this key's plane."
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Replaying the same key with the same body returns the original case rather than creating a second one. The same key with a different body is a 409. Stored for 24 hours. Strongly recommended: a duplicate grid registration costs a credit and has to be withdrawn by hand."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  },
                  "text": {
                    "type": "string"
                  },
                  "declared": {
                    "type": "string",
                    "description": "The declared object as JSON text."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The immutable response bundle.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InformationResponse"
                }
              }
            }
          },
          "413": {
            "description": "Material too large.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported media type.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/anmeldungen/{id}/information-requests/{requestId}/responses/{responseId}/submit": {
      "post": {
        "operationId": "submitInformationResponse",
        "summary": "Submit a completed response",
        "description": "Sandbox: resolves this request and opens one deterministic follow-up before approval, so tests exercise more than one round. Live: moves the response to `queued_for_operator`; it does not claim a VNB accepted it because live VNB transmission is operated by the case team. A later VNB question produces a new webhook request.",
        "tags": [
          "The case"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The case id, e.g. `anm_7Kq3ZmR8vT`. Ids always carry the `anm_` prefix, which is what keeps the static `14a` segment on the create path unambiguous against this parameter."
          },
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The `information_request.id` from the webhook, on this key's plane."
          },
          {
            "name": "responseId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The response id returned by POST /responses, on this key's plane."
          }
        ],
        "responses": {
          "200": {
            "description": "The response after submission or queuing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InformationResponse"
                }
              }
            }
          },
          "422": {
            "description": "The response is not ready to submit.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "`Authorization: Bearer wnz_live_…` on the live plane, `Authorization: Bearer wnz_test_…` on the sandbox. The prefix decides the plane; nothing else does. Both are minted in the console at https://www.wisnots.com/netzanmeldung/portal under Entwickler, and carry the `netz` scope."
      }
    },
    "schemas": {
      "Localised": {
        "type": "object",
        "description": "A short string in both languages. Display only; never a contract value.",
        "properties": {
          "de": {
            "type": "string"
          },
          "en": {
            "type": "string"
          }
        },
        "required": [
          "de",
          "en"
        ]
      },
      "Evidence": {
        "type": "object",
        "description": "Where an extracted value was read from. The quote is a verbatim span of the extracted document text, so a caller can show their own user the sentence behind a value instead of a confidence score nobody can act on.",
        "properties": {
          "material_id": {
            "type": "string"
          },
          "filename": {
            "type": "string"
          },
          "quote": {
            "type": "string"
          },
          "page": {
            "type": "integer",
            "minimum": 1
          }
        },
        "required": [
          "material_id",
          "quote"
        ]
      },
      "Site": {
        "type": "object",
        "description": "Where the installation physically is. A Postfach is not a valid site.",
        "properties": {
          "street": {
            "type": "string",
            "x-nr": 1002
          },
          "house_number": {
            "type": "string",
            "x-nr": 1003
          },
          "postal_code": {
            "type": "string",
            "pattern": "^[0-9]{5}$",
            "x-nr": 1007
          },
          "city": {
            "type": "string",
            "x-nr": 1008
          },
          "address_addition": {
            "type": "string",
            "x-nr": 1004
          },
          "district": {
            "type": "string",
            "x-nr": 1009,
            "description": "Ortsteil."
          },
          "address_kind": {
            "type": "string",
            "enum": [
              "postal",
              "parcel"
            ],
            "x-nr": 1001,
            "description": "Whether the site is given as a postal address or as Gemarkung and Flurstück. Derived from what you send when absent."
          }
        },
        "additionalProperties": false
      },
      "EndCustomer": {
        "type": "object",
        "description": "The Anschlussnutzer: the person or organisation operating the device.",
        "properties": {
          "first_name": {
            "type": "string",
            "x-nr": 1102
          },
          "last_name": {
            "type": "string",
            "x-nr": 1101,
            "description": "Name oder Firmenname: a company's name goes here, with no first name."
          },
          "email": {
            "type": "string",
            "format": "email",
            "x-nr": 1110
          },
          "phone": {
            "type": "string",
            "x-nr": 1109
          },
          "is_owner": {
            "type": "boolean",
            "description": "True when they own the property. False means a landlord consent may be required."
          },
          "street": {
            "type": "string",
            "x-nr": 1103,
            "description": "The Anschlussnutzer's own address, fields 1103 to 1108. Distinct from `site`: a landlord filing for a tenant's roof lives elsewhere. Never blocks; the site address does."
          },
          "house_number": {
            "type": "string",
            "x-nr": 1104
          },
          "address_addition": {
            "type": "string",
            "x-nr": 1105
          },
          "postal_code": {
            "type": "string",
            "x-nr": 1106
          },
          "city": {
            "type": "string",
            "x-nr": 1107
          },
          "district": {
            "type": "string",
            "x-nr": 1108
          },
          "iban": {
            "type": "string",
            "x-nr": 1111,
            "description": "PV only. Where the feed-in remuneration is paid."
          },
          "company_status": {
            "type": "string",
            "enum": [
              "in_difficulty",
              "open_recovery_claims",
              "neither"
            ],
            "x-nr": 1112,
            "description": "PV only. The EU state-aid declaration a company makes before it can receive EEG remuneration."
          }
        },
        "additionalProperties": false
      },
      "ContractHolder": {
        "type": "object",
        "description": "Anschlussnehmer, fields 1201 to 1210: the party holding the connection contract, when that is not the end customer using the installation. A landlord, typically. Leave the block out when they are the same person.",
        "properties": {
          "last_name": {
            "type": "string",
            "x-nr": 1201,
            "description": "Name oder Firmenname."
          },
          "first_name": {
            "type": "string",
            "x-nr": 1202
          },
          "street": {
            "type": "string",
            "x-nr": 1203
          },
          "house_number": {
            "type": "string",
            "x-nr": 1204
          },
          "postal_code": {
            "type": "string",
            "x-nr": 1206
          },
          "city": {
            "type": "string",
            "x-nr": 1207
          },
          "phone": {
            "type": "string",
            "x-nr": 1209
          },
          "email": {
            "type": "string",
            "format": "email",
            "x-nr": 1210
          }
        },
        "additionalProperties": false
      },
      "Billing": {
        "type": "object",
        "description": "Rechnungsempfänger, fields 1301 to 1310: who receives the invoice, when that is not the end customer. Leave the block out when it is.",
        "properties": {
          "name": {
            "type": "string",
            "x-nr": 1301,
            "description": "Name or company name."
          },
          "first_name": {
            "type": "string",
            "x-nr": 1302
          },
          "street": {
            "type": "string",
            "x-nr": 1303
          },
          "house_number": {
            "type": "string",
            "x-nr": 1304
          },
          "address_addition": {
            "type": "string",
            "x-nr": 1305
          },
          "postal_code": {
            "type": "string",
            "x-nr": 1306
          },
          "city": {
            "type": "string",
            "x-nr": 1307
          },
          "district": {
            "type": "string",
            "x-nr": 1308
          },
          "phone": {
            "type": "string",
            "x-nr": 1309
          },
          "email": {
            "type": "string",
            "format": "email",
            "x-nr": 1310
          }
        },
        "additionalProperties": false
      },
      "Installer": {
        "type": "object",
        "description": "The Elektrofachbetrieb. Named on the filing whether or not they are the API caller, because the grid operator's process expects them.",
        "properties": {
          "company": {
            "type": "string",
            "x-nr": 1401
          },
          "street": {
            "type": "string",
            "x-nr": 1402
          },
          "house_number": {
            "type": "string",
            "x-nr": 1403
          },
          "address_addition": {
            "type": "string",
            "x-nr": 1404
          },
          "postal_code": {
            "type": "string",
            "x-nr": 1405
          },
          "city": {
            "type": "string",
            "x-nr": 1406
          },
          "district": {
            "type": "string",
            "x-nr": 1407
          },
          "phone": {
            "type": "string",
            "x-nr": 1408
          },
          "email": {
            "type": "string",
            "format": "email",
            "x-nr": 1409
          },
          "registration_operator": {
            "type": "string",
            "x-nr": 1410,
            "description": "The grid operator the entry number was issued by. An entry in an Installateurverzeichnis is per operator, so the number alone does not identify it."
          },
          "registry_number": {
            "type": "string",
            "x-nr": 1411,
            "description": "Entry number in the grid operator's Installateurverzeichnis."
          },
          "contact_last_name": {
            "type": "string",
            "x-nr": 1501,
            "description": "The person to call about this case."
          },
          "contact_first_name": {
            "type": "string",
            "x-nr": 1502
          },
          "contact_phone": {
            "type": "string",
            "x-nr": 1503
          },
          "contact_email": {
            "type": "string",
            "format": "email",
            "x-nr": 1504
          },
          "salutation": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "first_name": {
            "type": "string"
          },
          "last_name": {
            "type": "string"
          },
          "care_of": {
            "type": "string"
          },
          "register_court_number": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "fax": {
            "type": "string"
          },
          "directory_reference": {
            "type": "string",
            "description": "The firm's entry in the grid operator's Installateurverzeichnis, where the portal picks it from that list rather than from the number."
          }
        },
        "additionalProperties": false
      },
      "Applicant": {
        "type": "object",
        "description": "The firm making the application, where the caller files on their customer's behalf. Optional; without it a filing goes out in the customer's own name. The national data set models no such party, so these fields carry no number.",
        "properties": {
          "salutation": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "first_name": {
            "type": "string"
          },
          "last_name": {
            "type": "string"
          },
          "care_of": {
            "type": "string"
          },
          "company": {
            "type": "string"
          },
          "register_court_number": {
            "type": "string"
          },
          "street": {
            "type": "string"
          },
          "house_number": {
            "type": "string"
          },
          "address_addition": {
            "type": "string"
          },
          "postal_code": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "district": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "phone": {
            "type": "string"
          },
          "fax": {
            "type": "string"
          },
          "dob": {
            "type": "string",
            "format": "date"
          },
          "mobile": {
            "type": "string"
          }
        },
        "additionalProperties": false
      },
      "Builder": {
        "type": "object",
        "description": "Anlagenerrichter, fields 1601 to 1609: the firm that built the installation, when that is not the Elektrofachbetrieb in `installer`. On a PV roof they are often two companies, and the standard asks for both.",
        "properties": {
          "company": {
            "type": "string",
            "x-nr": 1601
          },
          "street": {
            "type": "string",
            "x-nr": 1602
          },
          "house_number": {
            "type": "string",
            "x-nr": 1603
          },
          "address_addition": {
            "type": "string",
            "x-nr": 1604
          },
          "postal_code": {
            "type": "string",
            "x-nr": 1605
          },
          "city": {
            "type": "string",
            "x-nr": 1606
          },
          "district": {
            "type": "string",
            "x-nr": 1607
          },
          "phone": {
            "type": "string",
            "x-nr": 1608
          },
          "email": {
            "type": "string",
            "format": "email",
            "x-nr": 1609
          }
        },
        "additionalProperties": false
      },
      "FeedIn": {
        "type": "object",
        "description": "How the feed-in is sold and paid. PV and storage only; a § 14a case that sends it gets a 422 naming the field, because the standard does not ask a charge point how its export is remunerated.",
        "properties": {
          "sale_form": {
            "type": "string",
            "enum": [
              "free_of_charge",
              "subsidised_direct_marketing",
              "feed_in_tariff",
              "tenant_electricity",
              "other_direct_marketing",
              "unsubsidised"
            ],
            "x-nr": 2018,
            "description": "Gewünschte Veräußerungsform der Einspeisung."
          },
          "remuneration_notes": {
            "type": "string",
            "x-nr": 2019
          },
          "remote_control_proof": {
            "type": "string",
            "x-nr": 2020,
            "description": "PV only. Proof the plant can be remote-controlled for direct marketing."
          },
          "grid_operator_confirmation": {
            "type": "boolean",
            "x-nr": 2021,
            "description": "PV only."
          },
          "eeg_commissioning_at": {
            "type": "string",
            "format": "date",
            "x-nr": 2005,
            "description": "EEG-Inbetriebnahmedatum. May equal `connection.planned_commissioning_at`; the standard keeps them apart because the EEG date fixes the tariff."
          }
        },
        "additionalProperties": false
      },
      "Device": {
        "type": "object",
        "description": "The installation being registered. One object for all five Anlagentypen: a field that only one type asks is marked so, and sending it for another type is a 422 naming the field.",
        "properties": {
          "category": {
            "type": "string",
            "enum": [
              "charge_point",
              "heat_pump",
              "pv",
              "battery",
              "air_conditioning"
            ],
            "description": "Only on `POST /v1/anmeldungen/14a`, which pins no type. The typed routes have already answered it; sending a different value there is a 422."
          },
          "status": {
            "type": "string",
            "enum": [
              "planned",
              "installed",
              "in_operation"
            ],
            "description": "Drives which filings are derived. An installation already in operation needs no connection request, only the registration."
          },
          "installed_at": {
            "type": "string",
            "format": "date",
            "description": "Commissioning date. A date before 2024-01-01 makes this a Bestandsanlage, where § 14a participation is a voluntary opt-in rather than an obligation. Operational, not a field of the standard: the EEG-Inbetriebnahmedatum (field 2005) is `feed_in.eeg_commissioning_at` and exists only for PV and storage."
          },
          "count": {
            "type": "integer",
            "minimum": 1,
            "description": "Identical units on this connection. Fields 5001, 6001, 4005 and 7001; PV counts inverters instead, see `inverter_count`."
          },
          "manufacturer": {
            "type": "string",
            "description": "Fields 5002, 6002, 4001 and 7002. PV: per inverter, see `inverters`."
          },
          "model": {
            "type": "string",
            "description": "Fields 5003, 6003, 4002 and 7003."
          },
          "grid_draw_kw": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 100,
            "description": "Maximale Netzbezugsleistung. Charge point 5017, storage 4018, air conditioning 7014."
          },
          "separate_meter": {
            "type": "boolean",
            "description": "Messung über separaten Zähler. Fields 5011, 6010, 4012 and 7008."
          },
          "meter_exists": {
            "type": "boolean",
            "description": "Zähler bereits vorhanden. Charge point 5012, storage 4013, air conditioning 7009."
          },
          "existing_installation": {
            "type": "string",
            "enum": [
              "Ja (Anlage ohne Steuerung § 14a alte Fassung)",
              "Ja (Anlage mit Steuerung § 14a alte Fassung)",
              "Nein"
            ],
            "description": "Bestandsanlage vor dem 01.01.2024, in the operators' own wording. Fields 5014, 6013, 4015 and 7011."
          },
          "control_mode": {
            "type": "string",
            "enum": [
              "Direkt",
              "Energiemanagementsystem"
            ],
            "description": "Steuerungsart. Fields 5015, 6014, 4016 and 7012."
          },
          "tariff_module": {
            "type": "string",
            "enum": [
              "Modul 1",
              "Modul 2"
            ],
            "description": "Netzentgeltmodul. Fields 5016, 6015, 4017 and 7013."
          },
          "load_management": {
            "type": "string",
            "enum": [
              "Nein",
              "Ja (dynamisch)",
              "Ja (statisch)"
            ],
            "description": "Energie- bzw. Lastmanagement. Charge point 5019, storage 4020, air conditioning 7016."
          },
          "control_interface": {
            "type": "string",
            "enum": [
              "Relais",
              "Digital (EEBUS)"
            ],
            "description": "Storage 4023, air conditioning 7017."
          },
          "form_factor": {
            "type": "string",
            "enum": [
              "Ladesäule",
              "Ladebox",
              "Sonstiges"
            ],
            "x-nr": 5006,
            "description": "Charge point only."
          },
          "heat_pump_type": {
            "type": "string",
            "enum": [
              "air_water",
              "brine_water",
              "water_water",
              "other"
            ],
            "x-nr": 6004,
            "description": "Heat pump only."
          },
          "starting_current_a": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "Maximaler Anlaufstrom in A. Heat pump 6007, air conditioning 7005."
          },
          "electrical_power_kw": {
            "type": "number",
            "exclusiveMinimum": 0,
            "x-nr": 7004,
            "description": "Air conditioning only."
          },
          "inverter_count": {
            "type": "integer",
            "minimum": 1,
            "x-nr": 3008,
            "description": "PV only: identical inverters. Each one's own figures go in `inverters`."
          },
          "apparent_power_kva": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "Sum of the maximum apparent powers. PV 3009, storage 4006."
          },
          "active_power_kw": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "Sum of the maximum active powers. PV 3010, storage 4019 (feed-in)."
          },
          "module_power_kwp": {
            "type": "number",
            "exclusiveMinimum": 0,
            "x-nr": 3101,
            "description": "PV only: total module capacity."
          },
          "operating_mode": {
            "type": "string",
            "description": "Betriebsweise, in the operators' own wording. PV (3001): `Überschusseinspeisung`, `Volleinspeisung`, `Nulleinspeisung`. Storage (4008): `Kein Bezug und keine Einspeisung`, `Bezug und keine Einspeisung`, `Kein Bezug aber Einspeisung`, `Bezug und Einspeisung`."
          },
          "coupling": {
            "type": "string",
            "enum": [
              "Eigener Wechselrichter (AC-Kopplung)",
              "Gemeinsamer Wechselrichter mit Erzeugungsanlage (DC-Kopplung)"
            ],
            "x-nr": 4007,
            "description": "Storage only."
          },
          "storage_capacity_kwh": {
            "type": "number",
            "exclusiveMinimum": 0,
            "x-nr": 4021,
            "description": "Storage only."
          },
          "zerez_id": {
            "type": "string",
            "x-nr": 4022,
            "description": "Storage only. PV inverters carry theirs in `inverters`."
          }
        },
        "additionalProperties": false
      },
      "Inverter": {
        "type": "object",
        "description": "One inverter of a PV array. The standard asks these once per unit, so they travel as the `inverters` array and come back as `inverters.0.manufacturer` and so on.",
        "properties": {
          "manufacturer": {
            "type": "string",
            "x-nr": 3003
          },
          "model": {
            "type": "string",
            "x-nr": 3004
          },
          "apparent_power_kva": {
            "type": "number",
            "exclusiveMinimum": 0,
            "x-nr": 3006
          },
          "active_power_kw": {
            "type": "number",
            "exclusiveMinimum": 0,
            "x-nr": 3007
          },
          "zerez_id": {
            "type": "string",
            "x-nr": 3015
          }
        },
        "additionalProperties": false
      },
      "Protection": {
        "type": "object",
        "description": "NA-Schutz, fields 3301 to 3305. PV and storage only.",
        "properties": {
          "design": {
            "type": "string",
            "enum": [
              "Zentral am Zählerplatz",
              "Integriert",
              "Zentral am Zählerplatz und integriert in Kombination"
            ],
            "x-nr": 3301
          },
          "manufacturer": {
            "type": "string",
            "x-nr": 3302
          },
          "model": {
            "type": "string",
            "x-nr": 3303
          },
          "certificate": {
            "type": "string",
            "x-nr": 3304
          },
          "zerez_id": {
            "type": "string",
            "x-nr": 3305
          }
        },
        "additionalProperties": false
      },
      "Connection": {
        "type": "object",
        "description": "The grid connection the device hangs off.",
        "properties": {
          "meter_number": {
            "type": "string",
            "description": "Zählernummer. One name, the number follows the type: 5013, 6012, 3014, 4014 or 7010."
          },
          "location_in_plan": {
            "type": "string",
            "x-nr": 2003,
            "description": "Aufstellungsort im Lageplan."
          },
          "grid_draw_kw": {
            "type": "number",
            "exclusiveMinimum": 0,
            "x-nr": 2008,
            "description": "Netzwirksamer Leistungsbezug am Netzanschluss, the connection's total. Not the device's own, which is `device.grid_draw_kw`."
          },
          "metering_operator": {
            "type": "string",
            "x-nr": 2011,
            "description": "Charge point and heat pump."
          },
          "has_smart_meter": {
            "type": "boolean",
            "description": "An intelligentes Messsystem is installed. This is the main signal for who can perform the control, so supplying it often answers `section_14a.control_responsibility` for you."
          },
          "planned_commissioning_at": {
            "type": "string",
            "format": "date",
            "x-nr": 2004,
            "description": "Geplanter Inbetriebsetzungstermin, the day the plant is to go into operation. Optional: left out, the case is filed with the operator with the day 14 days after it was filed with us, on Berlin's calendar, or, if that day has passed when we prepare the filing, with the day 14 days after that; filing a case without it agrees to that."
          },
          "single_line_diagram": {
            "type": "string",
            "description": "Einpolige Darstellung des Messkonzepts. Field 2006 on a PV case, 2007 on the four § 14a families; one name, the number follows the family. A reference to the drawing, not the drawing: send that as `material`."
          },
          "tab_confirmed": {
            "type": "boolean",
            "x-nr": 2009,
            "description": "The installer confirms the operator's TAB for steuerbare Verbrauchseinrichtungen are met."
          },
          "fuse_change": {
            "type": "string",
            "enum": [
              "sufficient",
              "increase_required"
            ],
            "x-nr": 2022
          },
          "fuse_required_a": {
            "type": "number",
            "exclusiveMinimum": 0,
            "x-nr": 2023,
            "description": "In ampere."
          },
          "grey_power": {
            "type": "string",
            "enum": [
              "renewable_only",
              "grey_power"
            ],
            "x-nr": 2013,
            "description": "Charge point and storage only."
          },
          "grey_power_balancing_group": {
            "type": "string",
            "x-nr": 2014
          },
          "grey_power_marketer": {
            "type": "string",
            "x-nr": 2015
          },
          "existing_controllable_device": {
            "type": "string",
            "enum": [
              "none",
              "charge_point",
              "heat_pump",
              "air_conditioning",
              "battery"
            ],
            "x-nr": 2001,
            "description": "PV only. A § 14a device already on the connection."
          },
          "control_equipment": {
            "type": "string",
            "enum": [
              "new_default_mso",
              "new_competitive_mso",
              "existing"
            ],
            "x-nr": 2002,
            "description": "PV only. The § 9 EEG control equipment."
          },
          "metering_operator_choice": {
            "type": "string",
            "enum": [
              "default_mso",
              "competitive_mso"
            ],
            "x-nr": 2012,
            "description": "PV only."
          },
          "time_series_type_draw": {
            "type": "string",
            "enum": [
              "sls",
              "lgs",
              "other"
            ],
            "x-nr": 2016,
            "description": "Storage only."
          },
          "time_series_type_feed_in": {
            "type": "string",
            "enum": [
              "sol",
              "egs",
              "other"
            ],
            "x-nr": 2017,
            "description": "Storage only."
          }
        },
        "additionalProperties": false
      },
      "Consent": {
        "type": "object",
        "description": "The operator's agreement to be registered under § 14a. Filing the case is that agreement: the hand-over is the instruction to file, so nothing here is required and a case never waits for it. Send it when the operator agreed before the filing, and it is kept as the record of when and how.",
        "properties": {
          "given_by": {
            "type": "string",
            "enum": [
              "end_customer",
              "installer_on_behalf"
            ]
          },
          "given_at": {
            "type": "string",
            "format": "date-time"
          },
          "channel": {
            "type": "string",
            "description": "How the consent was given, in your own words: `phone`, `email`, `written`, or the name of your platform."
          },
          "reference": {
            "type": "string",
            "description": "Your own identifier for the record of consent: a call id, a mail id, a document id."
          }
        },
        "additionalProperties": false
      },
      "Section14a": {
        "type": "object",
        "description": "The § 14a specifics.",
        "properties": {
          "modules": {
            "type": "string",
            "example": "1",
            "description": "Which network-fee module is chosen, as text: `1` is the flat reduction and the ordinary case, `2` the percentage reduction on the energy price and excludes 1, `3` the time-variable network fee and combines with 1. Sent as `1` or as the operator's own wording, `Modul 1`."
          },
          "control_responsibility": {
            "type": "string",
            "enum": [
              "grid_operator",
              "metering_operator"
            ],
            "x-nr": 2010,
            "description": "Who performs the grid-oriented control: the grid operator with its own control equipment, or the metering point operator over the smart meter gateway."
          },
          "is_controllable": {
            "type": "boolean",
            "description": "Whether the device is a steuerbare Verbrauchseinrichtung at all. Charge point 5010, storage 4011, air conditioning 7007; a heat pump always is."
          },
          "critical_infrastructure": {
            "type": "boolean",
            "x-nr": 7006,
            "description": "Air conditioning only: serves betriebsnotwendige Zwecke or critical infrastructure, which exempts it from control."
          },
          "consent": {
            "$ref": "#/components/schemas/Consent"
          }
        },
        "additionalProperties": false
      },
      "Declared": {
        "type": "object",
        "description": "Everything the caller already knows. Every field is optional: a caller who knows nothing sends no `declared` at all and relies on `material`. Declared values are authoritative and are never overwritten by extraction.",
        "properties": {
          "site": {
            "$ref": "#/components/schemas/Site"
          },
          "end_customer": {
            "$ref": "#/components/schemas/EndCustomer"
          },
          "contract_holder": {
            "$ref": "#/components/schemas/ContractHolder"
          },
          "billing": {
            "$ref": "#/components/schemas/Billing"
          },
          "installer": {
            "$ref": "#/components/schemas/Installer"
          },
          "applicant": {
            "$ref": "#/components/schemas/Applicant"
          },
          "builder": {
            "$ref": "#/components/schemas/Builder"
          },
          "device": {
            "$ref": "#/components/schemas/Device"
          },
          "inverters": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Inverter"
            },
            "description": "PV only: one entry per inverter, in the order you count them."
          },
          "connection": {
            "$ref": "#/components/schemas/Connection"
          },
          "protection": {
            "$ref": "#/components/schemas/Protection"
          },
          "feed_in": {
            "$ref": "#/components/schemas/FeedIn"
          },
          "section_14a": {
            "$ref": "#/components/schemas/Section14a"
          },
          "nr": {
            "type": "object",
            "description": "Escape hatch to any field of the national data set by its number, including fields this API has not yet given a name. Keys are field numbers as strings. A named field and its numbered twin must not both be sent with different values; that is a 422.",
            "additionalProperties": {
              "type": [
                "string",
                "number",
                "boolean"
              ]
            },
            "propertyNames": {
              "pattern": "^[0-9]{4}$"
            },
            "example": {
              "1004": "Hinterhaus"
            }
          }
        },
        "additionalProperties": false
      },
      "Material": {
        "oneOf": [
          {
            "type": "object",
            "title": "Fixture (sandbox only)",
            "description": "A named document whose reading we already know. Costs nothing, makes no model call, and is identical every time, so a screen under construction can be rebuilt against it a hundred times a day.\n\nIt still travels the real path: the values land on the case exactly as a document we read would put them there, evidence and all, so nothing downstream behaves differently. A real upload in the sandbox still runs real extraction, because a fixture cannot teach you how YOUR documents behave.\n\nRefused under a live key: a live case built from a fixture would be a filing made of invented facts about a real customer.",
            "properties": {
              "fixture": {
                "type": "string",
                "enum": [
                  "wallbox-invoice-de",
                  "heat-pump-invoice-de",
                  "pv-invoice-de",
                  "battery-invoice-de",
                  "air-conditioning-invoice-de"
                ],
                "description": "One per Anlagentyp. Each is an installer's invoice for one installation, with the site and the letterhead at different addresses so a screen that confuses them shows it."
              }
            },
            "required": [
              "fixture"
            ],
            "additionalProperties": false
          },
          {
            "type": "object",
            "title": "Text",
            "properties": {
              "kind": {
                "type": "string",
                "const": "text"
              },
              "content": {
                "type": "string",
                "maxLength": 100000
              },
              "label": {
                "type": "string",
                "description": "What this is, for the audit trail. Never parsed as data."
              }
            },
            "required": [
              "kind",
              "content"
            ],
            "additionalProperties": false
          },
          {
            "type": "object",
            "title": "Inline file",
            "description": "For small files, so the common case is one call. Above the inline ceiling, register the document against the case and upload to the target it returns.",
            "properties": {
              "kind": {
                "type": "string",
                "const": "file"
              },
              "filename": {
                "type": "string"
              },
              "content_type": {
                "type": "string",
                "enum": [
                  "application/pdf",
                  "image/jpeg",
                  "image/png",
                  "image/webp",
                  "image/heic",
                  "text/plain"
                ]
              },
              "data": {
                "type": "string",
                "contentEncoding": "base64"
              }
            },
            "required": [
              "kind",
              "filename",
              "content_type",
              "data"
            ],
            "additionalProperties": false
          },
          {
            "type": "object",
            "title": "Already-uploaded file",
            "description": "Bytes already uploaded through the two-call flow. The caller sends back the storage id the upload target returned.",
            "properties": {
              "kind": {
                "type": "string",
                "const": "file"
              },
              "storage_id": {
                "type": "string"
              },
              "filename": {
                "type": "string"
              },
              "content_type": {
                "type": "string"
              }
            },
            "required": [
              "kind",
              "storage_id",
              "filename",
              "content_type"
            ],
            "additionalProperties": false
          }
        ],
        "description": "One piece of raw material. There is deliberately no `url` variant: fetching a caller-supplied URL server-side is a request-forgery surface, so bytes are either inlined or uploaded to a target we issue.\n\nA PDF too large for one vision read is cut into runs of consecutive pages and read run by run, up to 60 pages; the texts are joined in page order, so a quote in `evidence` can be found on its page. A photograph too large is shrunk. Only a single page that alone exceeds the reader's limit is unreadable, and the material says so."
      },
      "ResolvedField": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string",
            "description": "Dotted path into `declared`, e.g. `site.street`."
          },
          "nr": {
            "type": "integer",
            "description": "The field's number in the national data set."
          },
          "label": {
            "type": "object",
            "description": "A short string in both languages. Display only; never a contract value.",
            "properties": {
              "de": {
                "type": "string"
              },
              "en": {
                "type": "string"
              }
            },
            "required": [
              "de",
              "en"
            ]
          },
          "value": {},
          "unit": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "enum": [
              "declared",
              "extracted",
              "derived"
            ],
            "description": "Where the resolved value came from. `declared` was supplied by the caller and is never overwritten. `extracted` was read out of material. `derived` was computed by us from other values. Precedence when they disagree: declared > extracted > derived, and the losing values are reported in `conflicts` rather than dropped."
          },
          "confidence": {
            "type": "string",
            "enum": [
              "certain",
              "probable"
            ],
            "description": "Only meaningful for `extracted`. `probable` means the value was inferred rather than stated, for example a commissioning date taken from a delivery date."
          },
          "evidence": {
            "$ref": "#/components/schemas/Evidence"
          }
        },
        "required": [
          "path",
          "label",
          "value",
          "source"
        ]
      },
      "OpenField": {
        "type": "object",
        "description": "A field we still need. Labels, help text and options are the data set's own wording, the words the operators use, so a caller can put the question to their user in the same words the grid operator will use later.",
        "properties": {
          "path": {
            "type": "string"
          },
          "nr": {
            "type": "integer"
          },
          "label": {
            "type": "object",
            "description": "A short string in both languages. Display only; never a contract value.",
            "properties": {
              "de": {
                "type": "string"
              },
              "en": {
                "type": "string"
              }
            },
            "required": [
              "de",
              "en"
            ]
          },
          "help": {
            "type": "object",
            "description": "A short string in both languages. Display only; never a contract value.",
            "properties": {
              "de": {
                "type": "string"
              },
              "en": {
                "type": "string"
              }
            },
            "required": [
              "de",
              "en"
            ]
          },
          "hint": {
            "type": "object",
            "description": "A short string in both languages. Display only; never a contract value.",
            "properties": {
              "de": {
                "type": "string"
              },
              "en": {
                "type": "string"
              }
            },
            "required": [
              "de",
              "en"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "string",
              "number",
              "boolean",
              "date",
              "enum"
            ]
          },
          "unit": {
            "type": "string"
          },
          "options": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "value": {
                  "type": "string"
                },
                "label": {
                  "type": "object",
                  "description": "A short string in both languages. Display only; never a contract value.",
                  "properties": {
                    "de": {
                      "type": "string"
                    },
                    "en": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "de",
                    "en"
                  ]
                }
              }
            }
          },
          "operator_specific": {
            "type": "boolean",
            "description": "Present and true when this entry reflects the case's grid operator rather than the national data set, so you can tell what will not port to the next operator. Only ever appears under an operator-aware key; see `requirements` on the case.\n\nRead it together with `nr`: no number means a question the standard does not ask at all, and its path sits under `operator.`; a number means the standard's own question with this operator's answer set, at the standard's path, so nothing you already send has to be renamed."
          },
          "owed_by": {
            "type": "string",
            "enum": [
              "end_customer",
              "installer",
              "us",
              "operator"
            ],
            "description": "Who is expected to know this. It is what lets a caller route the question instead of guessing, and it is why an open list is not simply a validation error list."
          },
          "blocking": {
            "type": "boolean",
            "description": "True when the filing cannot be lodged without it. Non-blocking open fields are accepted: partial intake is deliberate, and we ask for the rest afterwards."
          }
        },
        "required": [
          "path",
          "label",
          "type",
          "owed_by",
          "blocking"
        ]
      },
      "Conflict": {
        "type": "object",
        "description": "Two sources gave different values for the same field. We do not silently pick one: a misdeclaration to a grid operator is worse than a question. Declaring the value settles it, because declared wins.",
        "properties": {
          "path": {
            "type": "string"
          },
          "nr": {
            "type": "integer"
          },
          "label": {
            "type": "object",
            "description": "A short string in both languages. Display only; never a contract value.",
            "properties": {
              "de": {
                "type": "string"
              },
              "en": {
                "type": "string"
              }
            },
            "required": [
              "de",
              "en"
            ]
          },
          "blocking": {
            "type": "boolean"
          },
          "message": {
            "type": "object",
            "description": "A short string in both languages. Display only; never a contract value.",
            "properties": {
              "de": {
                "type": "string"
              },
              "en": {
                "type": "string"
              }
            },
            "required": [
              "de",
              "en"
            ]
          },
          "candidates": {
            "type": "array",
            "minItems": 2,
            "items": {
              "type": "object",
              "properties": {
                "value": {},
                "source": {
                  "type": "string",
                  "enum": [
                    "declared",
                    "extracted",
                    "derived"
                  ],
                  "description": "Where the resolved value came from. `declared` was supplied by the caller and is never overwritten. `extracted` was read out of material. `derived` was computed by us from other values. Precedence when they disagree: declared > extracted > derived, and the losing values are reported in `conflicts` rather than dropped."
                },
                "evidence": {
                  "$ref": "#/components/schemas/Evidence"
                }
              }
            }
          }
        },
        "required": [
          "path",
          "label",
          "candidates",
          "blocking"
        ]
      },
      "Notice": {
        "type": "object",
        "description": "Something worth knowing that does not block anything.",
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "existing_operator_approval_found",
              "existing_installation",
              "multiple_installations_detected",
              "material_unreadable",
              "device_not_controllable",
              "module_choice_defaulted"
            ]
          },
          "message": {
            "type": "object",
            "description": "A short string in both languages. Display only; never a contract value.",
            "properties": {
              "de": {
                "type": "string"
              },
              "en": {
                "type": "string"
              }
            },
            "required": [
              "de",
              "en"
            ]
          },
          "detail": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "required": [
          "code",
          "message"
        ]
      },
      "MaterialSummary": {
        "type": "object",
        "description": "One piece of material and how far its reading got. A `file` carries `filename`, `content_type` and `bytes`; a `text` its `label`; a `fixture` its id. `status` walks `pending` (queued), `reading` (the reader has it), then `read` or `unreadable`. Finished means one of the last two, and the case's own `status` is `extracting` for as long as any entry is in the first two: poll the case's `status`, not the first entry that reads `read`.",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "text",
              "file",
              "fixture"
            ]
          },
          "filename": {
            "type": "string",
            "description": "A file's name, as sent."
          },
          "label": {
            "type": "string",
            "description": "A text's label, as sent; a fixture's title."
          },
          "content_type": {
            "type": "string",
            "description": "A file's media type, as the bytes were read."
          },
          "bytes": {
            "type": "integer",
            "description": "A file's size."
          },
          "fixture": {
            "type": "string",
            "description": "The fixture's id, on a fixture."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "reading",
              "read",
              "unreadable",
              "rejected"
            ],
            "description": "`pending` and `reading` both mean wait. `read`: text taken and values resolved onto the case. `unreadable`: the reader could not read it, and the case says so. `rejected` is reserved: today a file the reader cannot take is refused at upload, with a 415, and never becomes material."
          },
          "extracted_chars": {
            "type": "integer",
            "description": "How much text the reader took, once `read`."
          },
          "reject_reason": {
            "type": "string",
            "enum": [
              "too_small",
              "too_large",
              "wrong_format",
              "unreadable"
            ],
            "description": "Reserved with `rejected`; not produced today."
          }
        },
        "required": [
          "id",
          "kind",
          "status"
        ]
      },
      "Filing": {
        "type": "object",
        "description": "What we lodge on the caller's behalf. Always a response field, never a request field: the caller says what they installed, we decide what must be filed.",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "section_14a",
              "commissioning_notice",
              "grid_connection_request",
              "meter_application",
              "change_notification"
            ]
          },
          "stage": {
            "type": "string"
          },
          "stage_label": {
            "type": "object",
            "description": "A short string in both languages. Display only; never a contract value.",
            "properties": {
              "de": {
                "type": "string"
              },
              "en": {
                "type": "string"
              }
            },
            "required": [
              "de",
              "en"
            ]
          },
          "operator_ref": {
            "type": [
              "string",
              "null"
            ]
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "kind",
          "stage"
        ]
      },
      "Case": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "external_ref": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your identifier, echoed everywhere."
          },
          "installer_ref": {
            "type": [
              "string",
              "null"
            ],
            "description": "The installer firm the case is filed for, as you named it. Null when your account is the installer."
          },
          "operator_login": {
            "type": [
              "object",
              "null"
            ],
            "description": "The installer's grid operator login for this case, and the installer's link. Null until the case has a link: one that names its installer (`installer_ref`), or one we issued for it.\n\nEach installer has its own login at each operator, used for its cases there and nothing else. `link` is the installer's own page for handing in grid operator, login and password, encrypted, as many as it has: the same link for all of its cases, so send it once, and again when the status says the login is missing or was refused. Nothing to sign up for, and no consent to give: filing the case is the consent.\n\nA login handed in never replaces an earlier one: it is kept as a new version and used from then on. We can revoke a link; the case then carries its successor, and logins handed in through the revoked one are no longer used.\n\nThe link is in the body on live cases too, unlike the signature link. It can only add a login, never read or replace one, and getting it to the installer is the point.\n\n`operator` and `status` are null while the case's operator is not known; the installer names it on the page. Statuses: `missing` (none yet), `held`, `unusable` (the operator's portal refused it; ask for it again), `not_needed` (the operator takes filings by mail). `installer` is null when your account is the installer. `case.operator_login_missing` fires when a case moves into `missing` or `unusable`.",
            "properties": {
              "operator": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "key": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  }
                },
                "required": [
                  "key",
                  "name"
                ]
              },
              "installer": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "ref": {
                    "type": "string"
                  },
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "required": [
                  "ref",
                  "name"
                ]
              },
              "status": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "missing",
                  "held",
                  "unusable",
                  "not_needed",
                  null
                ]
              },
              "link": {
                "type": "string",
                "format": "uri"
              }
            },
            "required": [
              "operator",
              "installer",
              "status",
              "link"
            ]
          },
          "mode": {
            "type": "string",
            "enum": [
              "live",
              "test"
            ],
            "description": "Which plane this case is on, decided by the API key that created it and never changed afterwards. The id says the same thing (`anm_…` against `anm_test_…`), and saying it twice is deliberate: a screenshot of a sandbox case should be unmistakable.\n\nA key of one mode cannot read, patch, file or discard a case of the other. That is refused on the id, before any lookup, so a mis-wired environment is one 404 rather than a half-finished filing."
          },
          "kind": {
            "type": "string",
            "enum": [
              "section_14a",
              "pv",
              "storage"
            ],
            "description": "The filing family, fixed by the create path and never changed. It decides which questions the case has to answer.\n\nA FAMILY IS NOT A DEVICE. `section_14a` is one family covering four devices; `pv` is a different one: generation, with no steuerbare Verbrauchseinrichtung at all and payout fields § 14a never touches. It is also not the same as the filings: a § 14a intake can still resolve to more than one filing."
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "extracting",
              "needs_input",
              "ready",
              "filing",
              "submitted",
              "confirmed",
              "rejected",
              "cancelled"
            ],
            "description": "draft: created, nothing read yet. extracting: material is being read. needs_input: something blocking is open or contradicted. ready: nothing blocking. filing: we are lodging it. submitted: lodged with the operator, and still so while the customer signs and the operator considers. confirmed: the operator confirmed the registration. rejected: the operator refused. cancelled: discarded before filing.\n\nCoarse on purpose. The operator's finer stage (`waiting_operator`, `info_needed_installer`, ...) travels in the `case.status_changed` events, where a change is worth a webhook; the case itself answers the one question a screen asks, which is whether it is done and how."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "operator": {
            "type": [
              "object",
              "null"
            ],
            "description": "The grid operator this case is bound for. Null until one is named. It is taken from the `operator` field you send on create, never guessed from the address: an operator guessed wrong changes which questions the case has to answer.",
            "properties": {
              "key": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "key",
              "name"
            ]
          },
          "signature": {
            "type": [
              "object",
              "null"
            ],
            "description": "Where the signature stands. Null until one is asked for.\n\n**`file` does not finish the job.** Two signatures on the § 14a form are never ours (the customer's and the eingetragene verantwortliche Fachkraft's), so filing produces the document and routes it for signature by email. A caller who saw only `submitted` would tell their user the filing had gone out while it is waiting on the end customer.\n\n`sent_as` is the From header as it actually went, and `sent_as_kind` says whose domain that was. A firm with a verified sending domain sends as itself, because the end customer has heard of their electrician and has not heard of us; everyone else sends as ours. Either way the reply lands in a mailbox we read, so the signed form comes back to the case.\n\nStates: `pending` (queued), `sent`, `simulated` (the sandbox reached the point of sending and deliberately did not: the link still works), `failed` (with `failure_reason`), `opened`, `signed`, `superseded` (a newer request replaced it and this link no longer works).\n\nOn a live case the link itself is never in this object. It is a capability: whoever holds it can download the customer's form until it expires, and a response body is somewhere it would be logged. A sandbox case carries `link`, because there the form is a watermarked rehearsal and you are the customer: open it, see what they would see, and sign it to walk the rest of the sequence.",
            "properties": {
              "link": {
                "type": "string",
                "format": "uri",
                "description": "SANDBOX ONLY. The customer's link to the form, as the signature email would carry it. Never present on a live case."
              },
              "state": {
                "type": "string",
                "enum": [
                  "pending",
                  "sent",
                  "simulated",
                  "failed",
                  "opened",
                  "signed",
                  "superseded"
                ]
              },
              "sent_as": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "sent_as_kind": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "own",
                  "house",
                  null
                ]
              },
              "requested_at": {
                "type": "string",
                "format": "date-time"
              },
              "sent_at": {
                "type": "string",
                "format": "date-time"
              },
              "opened_at": {
                "type": "string",
                "format": "date-time"
              },
              "expires_at": {
                "type": "string",
                "format": "date-time"
              },
              "failure_reason": {
                "type": "string"
              }
            },
            "required": [
              "state",
              "requested_at",
              "expires_at"
            ]
          },
          "requirements": {
            "type": "object",
            "description": "Which requirements this API key is answered under, and how much of the operator's own is still outstanding.\n\n`surface` is `universal` or `operator`, and it is a property of your KEY, not of a request. Under `universal` you see the national data set and nothing else, so your integration is identical for every grid operator in Germany and we chase what that operator additionally asks. Under `operator` those questions are added to `open[]`, marked `operator_specific`, so you can collect everything in one pass; the price is that your field list now varies by operator.\n\n`operator_outstanding` counts the same thing under both, and it is why a universal key can trust an empty `open[]`: it is the operator's required questions that have no answer yet. While it is above zero the case cannot be filed, whether or not you can see the questions. Ask for an operator-aware key if you would rather answer them yourself.",
            "properties": {
              "surface": {
                "type": "string",
                "enum": [
                  "universal",
                  "operator"
                ]
              },
              "operator_outstanding": {
                "type": "integer"
              }
            },
            "required": [
              "surface",
              "operator_outstanding"
            ]
          },
          "price": {
            "type": "object",
            "properties": {
              "credits": {
                "type": "integer"
              },
              "charged": {
                "type": "boolean"
              },
              "charged_at": {
                "type": "string",
                "format": "date-time"
              }
            },
            "required": [
              "credits",
              "charged"
            ],
            "description": "One installation costs one credit, charged when the case is filed, not while it is a draft."
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ResolvedField"
            }
          },
          "open": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OpenField"
            }
          },
          "conflicts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Conflict"
            }
          },
          "rejected": {
            "type": "array",
            "description": "Values you sent that could not be used, each with the reason. A field the device family does not have, a value that will not parse as its type, one supplied twice under two names with two answers. The case is still created (the rest of your declaration is good), and nothing here is silently discarded.",
            "items": {
              "type": "object",
              "properties": {
                "path": {
                  "type": "string"
                },
                "reason": {
                  "type": "string"
                }
              },
              "required": [
                "path",
                "reason"
              ]
            }
          },
          "materials": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MaterialSummary"
            }
          }
        },
        "required": [
          "id",
          "mode",
          "status",
          "created_at",
          "updated_at",
          "price",
          "requirements"
        ]
      },
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem detail. `code` is a closed enum and is the field to branch on.",
        "properties": {
          "type": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "code": {
            "type": "string",
            "enum": [
              "unauthenticated",
              "forbidden_scope",
              "not_found",
              "validation_failed",
              "idempotency_conflict",
              "no_credits",
              "rate_limited",
              "material_too_large",
              "unsupported_media_type",
              "case_already_filed",
              "case_incomplete",
              "internal"
            ]
          },
          "detail": {
            "type": "string"
          },
          "instance": {
            "type": "string"
          },
          "case": {
            "type": "string",
            "description": "The case this refusal left behind, where the call created one and then refused. `POST /v1/anmeldungen/{typ}` with `intent: \"file\"` creates the case and then files it, and the two are not one act: a refusal of the FILING (no credits, the sandbox ceiling) leaves the case standing as a draft. Finish it with `POST /v1/anmeldungen/{id}/file` rather than sending the create again, which would open a second case. Absent on a refusal that created nothing."
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "path": {
                  "type": "string"
                },
                "code": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        },
        "required": [
          "type",
          "title",
          "status",
          "code"
        ]
      },
      "CreateRequest": {
        "type": "object",
        "description": "At least one of `declared` and `material` must be present. Sending both is the ordinary case and they compose: material fills what declared did not state.",
        "properties": {
          "external_ref": {
            "type": "string",
            "maxLength": 120
          },
          "installer_ref": {
            "type": "string",
            "maxLength": 120,
            "description": "Your own stable id for the installer firm this case is filed for, when one account files for many (a platform). Its grid operator logins are that installer's alone: never another installer's, never your account's own. Compared exactly, so send the same id for the same firm every time. Leave it out when your account is the installer."
          },
          "operator": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9-]{0,62}$",
            "description": "The grid operator's key, for example `hen` for Hamburger Energienetze. Optional: without one the case is judged against the national data set alone. With one, that operator's own requirements come into scope; see `requirements` on the case. `GET /v1/anmeldungen/14a/fields` lists the fields for your key's surface.\n\n`sandbox` is a fictional grid operator with a form that never changes, so a sandbox integration does not break on the day a real operator reissues theirs. Sandbox keys only; a live filing bound for an operator that does not exist could never be sent. Real operators stay selectable in the sandbox for anyone who wants today's requirements."
          },
          "intent": {
            "type": "string",
            "enum": [
              "draft",
              "file"
            ],
            "default": "draft",
            "description": "`draft` costs nothing and returns what is still open. `file` lodges the case if nothing blocking is open, and otherwise returns `needs_input` without charging a credit. Asking to file an incomplete case is not an error.\n\nThe case is created first and filed second, so a refusal of the FILING alone (no credits, the sandbox ceiling) leaves the case standing as a draft. The problem body then carries its id in `case`: finish it with `POST /v1/anmeldungen/{id}/file` rather than sending the create again."
          },
          "declared": {
            "$ref": "#/components/schemas/Declared"
          },
          "material": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Material"
            },
            "maxItems": 30
          },
          "sandbox_outcome": {
            "type": "string",
            "enum": [
              "accepted",
              "rejected",
              "needs_more_information"
            ],
            "default": "accepted",
            "description": "With `intent: file`, exactly as on `POST /{id}/file`. SANDBOX ONLY. What the grid operator decides. If the sandbox always accepted, nobody would build the rejection path, so you choose, and the case lands in the same stage a live one will, with a stated reason.\n\nUnder a live key this is a 422 naming the field, not an ignore. A live filing's outcome comes from the operator."
          },
          "sandbox_pace": {
            "type": "string",
            "enum": [
              "instant",
              "staged"
            ],
            "default": "instant",
            "description": "With `intent: file`, exactly as on `POST /{id}/file`. SANDBOX ONLY. How fast the sandbox operator answers.\n\n`instant` answers inside the filing call, which is what a test suite wants. `staged` is the one thing an instant answer cannot show: a live case is filed, waits, and is answered later. Staged walks `submitted`, `waiting_operator` and then `sandbox_outcome` a few seconds apart on a timer, each step a `case.status_changed` event your webhook receives in turn, so a receiver that has only ever seen the answer arrive inside the filing call gets to test the wait.\n\nUnder a live key this is a 422 naming the field. A live filing is answered when the grid operator answers."
          }
        },
        "additionalProperties": false
      },
      "PatchRequest": {
        "type": "object",
        "description": "Adds or corrects declared values. Merges rather than replaces: omitting a field leaves it alone. Sending null clears it back to whatever extraction found.",
        "properties": {
          "external_ref": {
            "type": "string"
          },
          "installer_ref": {
            "type": "string",
            "maxLength": 120,
            "description": "Names, or changes, the installer firm the case is filed for, as on create. Unlike `declared`, it may be sent for a filed case too: the installer's login is used after filing."
          },
          "declared": {
            "$ref": "#/components/schemas/Declared"
          }
        },
        "additionalProperties": false
      },
      "InformationMissing": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string"
          },
          "label": {
            "$ref": "#/components/schemas/Localised"
          },
          "help": {
            "$ref": "#/components/schemas/Localised"
          },
          "type": {
            "type": "string",
            "enum": [
              "field",
              "document",
              "text"
            ]
          },
          "required": {
            "type": "boolean"
          },
          "owed_by": {
            "type": "string",
            "enum": [
              "installer",
              "end_customer"
            ]
          }
        },
        "required": [
          "path",
          "label",
          "type",
          "required",
          "owed_by"
        ]
      },
      "InformationRequest": {
        "type": "object",
        "description": "An immutable operator request after filing. A later follow-up is a new sequence, never a mutation of this request.",
        "properties": {
          "id": {
            "type": "string",
            "description": "`info_…` live, `info_test_…` sandbox."
          },
          "sequence": {
            "type": "integer",
            "minimum": 1
          },
          "revision": {
            "type": "integer",
            "minimum": 1
          },
          "source": {
            "type": "string",
            "enum": [
              "operator",
              "sandbox"
            ]
          },
          "state": {
            "type": "string",
            "enum": [
              "open",
              "processing",
              "ready_to_submit",
              "queued_for_operator",
              "resolved",
              "cancelled"
            ]
          },
          "active": {
            "type": "boolean"
          },
          "missing": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InformationMissing"
            }
          },
          "note": {
            "type": [
              "string",
              "null"
            ]
          },
          "opened_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "closed_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "sequence",
          "revision",
          "source",
          "state",
          "active",
          "missing",
          "note",
          "opened_at",
          "updated_at"
        ]
      },
      "InformationRequestSnapshot": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "`info_…` live, `info_test_…` sandbox."
          },
          "sequence": {
            "type": "integer",
            "minimum": 1
          },
          "revision": {
            "type": "integer",
            "minimum": 1
          },
          "source": {
            "type": "string",
            "enum": [
              "operator",
              "sandbox"
            ]
          },
          "missing": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InformationMissing"
            }
          }
        },
        "required": [
          "id",
          "sequence",
          "revision",
          "source",
          "missing"
        ]
      },
      "InformationResponseRequest": {
        "type": "object",
        "description": "Only the field paths and material kinds named by this information request are accepted. At least one of `declared` or `material` is required.",
        "properties": {
          "declared": {
            "$ref": "#/components/schemas/Declared"
          },
          "material": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Material"
            },
            "maxItems": 30
          }
        },
        "additionalProperties": false
      },
      "InformationResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "`resp_…` live, `resp_test_…` sandbox."
          },
          "state": {
            "type": "string",
            "enum": [
              "processing",
              "ready_to_submit",
              "queued_for_operator",
              "submitted"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "state",
          "created_at",
          "updated_at"
        ]
      }
    },
    "examples": {
      "CreateSandbox": {
        "summary": "Fixture: a hosted invoice, free, no model call",
        "description": "Send this one first. `fixture` names a document we host and have already read, so nothing is uploaded and no model call is charged for reading it, and a screen under construction can rebuild against it all day.\n\nThe result is a real case in the sandbox plane, with the same fields, the same quotes and the same shape a live call returns. Nothing it does can file, charge or email anybody.",
        "value": {
          "external_ref": "ERP-4711",
          "intent": "draft",
          "material": [
            {
              "fixture": "wallbox-invoice-de"
            }
          ]
        }
      },
      "MaterialUploaded": {
        "summary": "Uploaded: the storage id an upload target answered with",
        "value": {
          "material": [
            {
              "kind": "file",
              "storage_id": "kg21x9q8v0s5c7b2m4n6p8r0t2w4y6a",
              "filename": "rechnung.pdf",
              "content_type": "application/pdf"
            }
          ]
        }
      },
      "MaterialInline": {
        "summary": "Inline: a small file as base64 in `data`",
        "description": "The `data` here is a PDF header and nothing after it: valid base64, far too little to be a document.",
        "value": {
          "material": [
            {
              "kind": "file",
              "filename": "typenschild.pdf",
              "content_type": "application/pdf",
              "data": "JVBERi0xLjQK"
            }
          ]
        }
      },
      "MaterialText": {
        "summary": "Text: a pasted customer mail",
        "value": {
          "material": [
            {
              "kind": "text",
              "label": "Kundenmail",
              "content": "Hallo, anbei die Unterlagen zur PV-Anlage. Die Module sind seit Juni montiert.\n\nViele Gruesse\nInes Kaufmann"
            }
          ]
        }
      },
      "CreatePvFixture": {
        "summary": "Fixture: a hosted PV invoice, free, no model call",
        "value": {
          "external_ref": "ERP-5101",
          "intent": "draft",
          "material": [
            {
              "fixture": "pv-invoice-de"
            }
          ]
        }
      },
      "CreatePvRaw": {
        "summary": "Raw: the customer's mail and two files, nothing structured",
        "description": "The shape a back office actually has: a pasted mail and the attachments, no fields. Every value comes back `extracted`, each with the quote it was read from. The two `data` values here are a PDF header and a JPEG header and nothing after: valid base64, sendable, far too little to be documents, so they read as unreadable and the mail carries the case. Send real files and they read for real, 25 a day on the sandbox.",
        "value": {
          "external_ref": "ERP-5103",
          "intent": "draft",
          "material": [
            {
              "kind": "text",
              "label": "Kundenmail",
              "content": "Hallo,\n\nanbei die Rechnung und ein Foto vom Typenschild des Wechselrichters zu unserer neuen PV-Anlage (9,84 kWp, ein Wechselrichter Fronius Symo GEN24 10.0 Plus). Zaehlernummer ist 1ESY1189004412. Bitte melden Sie die Anlage beim Netzbetreiber an, wir moechten die Einspeiseverguetung nach EEG.\n\nViele Gruesse\nInes Kaufmann\nHeinrichstrasse 118, 64283 Darmstadt"
            },
            {
              "kind": "file",
              "filename": "rechnung-sonnenstrom.pdf",
              "content_type": "application/pdf",
              "data": "JVBERi0xLjQK"
            },
            {
              "kind": "file",
              "filename": "typenschild-wechselrichter.jpg",
              "content_type": "image/jpeg",
              "data": "/9j/4AAQSkZJRgAB"
            }
          ]
        }
      },
      "CreatePvDeclared": {
        "summary": "Declared: fields mapped from your CRM, one inverter, 9.84 kWp",
        "value": {
          "external_ref": "ERP-5102",
          "intent": "draft",
          "declared": {
            "site": {
              "street": "Heinrichstrasse",
              "house_number": "118",
              "postal_code": "64283",
              "city": "Darmstadt"
            },
            "end_customer": {
              "first_name": "Ines",
              "last_name": "Kaufmann",
              "email": "i.kaufmann@example.de",
              "phone": "+49 6151 5550188",
              "is_owner": true
            },
            "installer": {
              "company": "Sonnenstrom Hessen GmbH",
              "street": "Rheinstrasse",
              "house_number": "40",
              "postal_code": "64293",
              "city": "Darmstadt",
              "email": "info@sonnenstrom-hessen.example"
            },
            "device": {
              "inverter_count": 1,
              "apparent_power_kva": 10,
              "active_power_kw": 10,
              "module_power_kwp": 9.84,
              "installed_at": "2026-06-12",
              "status": "planned"
            },
            "inverters": [
              {
                "manufacturer": "Fronius",
                "model": "Symo GEN24 10.0 Plus",
                "apparent_power_kva": 10,
                "active_power_kw": 10
              }
            ],
            "protection": {
              "manufacturer": "Ziehl",
              "model": "UFR1001E"
            },
            "connection": {
              "meter_number": "1ESY1189004412"
            },
            "feed_in": {
              "sale_form": "feed_in_tariff"
            }
          }
        }
      },
      "CreateSpeicherFixture": {
        "summary": "Fixture: a hosted storage invoice, free, no model call",
        "value": {
          "external_ref": "ERP-5201",
          "intent": "draft",
          "material": [
            {
              "fixture": "battery-invoice-de"
            }
          ]
        }
      },
      "CreateSpeicherRaw": {
        "summary": "Raw: the customer's mail and two files, nothing structured",
        "description": "The shape a back office actually has: a pasted mail and the attachments, no fields. Every value comes back `extracted`, each with the quote it was read from. The two `data` values here are a PDF header and a JPEG header and nothing after: valid base64, sendable, far too little to be documents, so they read as unreadable and the mail carries the case. Send real files and they read for real, 25 a day on the sandbox.",
        "value": {
          "external_ref": "ERP-5203",
          "intent": "draft",
          "material": [
            {
              "kind": "text",
              "label": "Kundenmail",
              "content": "Hallo,\n\nim Anhang Rechnung und Typenschild zu unserem Stromspeicher (BYD Battery-Box Premium HVS 10.2, 10,24 kWh, eigener Wechselrichter). Zaehlernummer 1ESY1170551234. Der Speicher soll nach 14a EnWG mit Modul 1 angemeldet werden.\n\nViele Gruesse\nJonas Petersen\nKanalufer 9, 24106 Kiel"
            },
            {
              "kind": "file",
              "filename": "rechnung-nordkap.pdf",
              "content_type": "application/pdf",
              "data": "JVBERi0xLjQK"
            },
            {
              "kind": "file",
              "filename": "typenschild-speicher.jpg",
              "content_type": "image/jpeg",
              "data": "/9j/4AAQSkZJRgAB"
            }
          ]
        }
      },
      "CreateSpeicherDeclared": {
        "summary": "Declared: fields mapped from your CRM, 10.24 kWh, AC-coupled",
        "value": {
          "external_ref": "ERP-5202",
          "intent": "draft",
          "declared": {
            "site": {
              "street": "Kanalufer",
              "house_number": "9",
              "postal_code": "24106",
              "city": "Kiel"
            },
            "end_customer": {
              "first_name": "Jonas",
              "last_name": "Petersen",
              "email": "j.petersen@example.de",
              "phone": "+49 431 5550172",
              "is_owner": true
            },
            "installer": {
              "company": "Elektro Nordkap GmbH",
              "email": "kontakt@elektro-nordkap.example"
            },
            "device": {
              "count": 1,
              "manufacturer": "BYD",
              "model": "Battery-Box Premium HVS 10.2",
              "apparent_power_kva": 5,
              "active_power_kw": 5,
              "storage_capacity_kwh": 10.24,
              "coupling": "Eigener Wechselrichter (AC-Kopplung)",
              "installed_at": "2026-07-02",
              "status": "planned"
            },
            "section_14a": {
              "is_controllable": true,
              "control_responsibility": "metering_operator",
              "modules": "1",
              "consent": {
                "given_by": "end_customer",
                "given_at": "2026-07-01T09:30:00Z",
                "channel": "email"
              }
            },
            "connection": {
              "meter_number": "1ESY1170551234"
            }
          }
        }
      },
      "CreateWaermepumpeFixture": {
        "summary": "Fixture: a hosted heat pump invoice, free, no model call",
        "value": {
          "external_ref": "ERP-5301",
          "intent": "draft",
          "material": [
            {
              "fixture": "heat-pump-invoice-de"
            }
          ]
        }
      },
      "CreateWaermepumpeRaw": {
        "summary": "Raw: the customer's mail and two files, nothing structured",
        "description": "The shape a back office actually has: a pasted mail and the attachments, no fields. Every value comes back `extracted`, each with the quote it was read from. The two `data` values here are a PDF header and a JPEG header and nothing after: valid base64, sendable, far too little to be documents, so they read as unreadable and the mail carries the case. Send real files and they read for real, 25 a day on the sandbox.",
        "value": {
          "external_ref": "ERP-5303",
          "intent": "draft",
          "material": [
            {
              "kind": "text",
              "label": "Kundenmail",
              "content": "Hallo,\n\nanbei Rechnung und Typenschild unserer Waermepumpe (Vaillant aroTHERM plus VWL 75/6 A, Luft-Wasser). Sie laeuft seit Mai. Zaehlernummer 1ESY1170099887. Bitte Modul 1 nach 14a EnWG anmelden, die Steuerung uebernimmt der Netzbetreiber.\n\nViele Gruesse\nPetra Ahlers\nWiesenweg 3, 22607 Hamburg"
            },
            {
              "kind": "file",
              "filename": "rechnung-nordlicht.pdf",
              "content_type": "application/pdf",
              "data": "JVBERi0xLjQK"
            },
            {
              "kind": "file",
              "filename": "typenschild-waermepumpe.jpg",
              "content_type": "image/jpeg",
              "data": "/9j/4AAQSkZJRgAB"
            }
          ]
        }
      },
      "CreateWaermepumpeDeclared": {
        "summary": "Declared: fields mapped from your CRM, air to water, 12 A starting current",
        "value": {
          "external_ref": "ERP-5302",
          "intent": "draft",
          "declared": {
            "site": {
              "street": "Wiesenweg",
              "house_number": "3",
              "postal_code": "22607",
              "city": "Hamburg"
            },
            "end_customer": {
              "first_name": "Petra",
              "last_name": "Ahlers",
              "email": "p.ahlers@example.de",
              "phone": "+49 40 5550199",
              "is_owner": true
            },
            "installer": {
              "company": "Sanitaer & Waerme Nordlicht GmbH",
              "email": "buero@nordlicht-waerme.example"
            },
            "device": {
              "count": 1,
              "manufacturer": "Vaillant",
              "model": "aroTHERM plus VWL 75/6 A",
              "heat_pump_type": "air_water",
              "starting_current_a": 12,
              "installed_at": "2026-05-20",
              "status": "installed"
            },
            "section_14a": {
              "control_responsibility": "grid_operator",
              "modules": "1",
              "consent": {
                "given_by": "end_customer",
                "given_at": "2026-05-18T09:30:00Z",
                "channel": "phone"
              }
            },
            "connection": {
              "meter_number": "1ESY1170099887"
            }
          }
        }
      },
      "CreateLadeeinrichtungFixture": {
        "summary": "Fixture: a hosted wallbox invoice, free, no model call",
        "value": {
          "external_ref": "ERP-5401",
          "intent": "draft",
          "material": [
            {
              "fixture": "wallbox-invoice-de"
            }
          ]
        }
      },
      "CreateLadeeinrichtungRaw": {
        "summary": "Raw: the customer's mail and two files, nothing structured",
        "description": "The shape a back office actually has: a pasted mail and the attachments, no fields. Every value comes back `extracted`, each with the quote it was read from. The two `data` values here are a PDF header and a JPEG header and nothing after: valid base64, sendable, far too little to be documents, so they read as unreadable and the mail carries the case. Send real files and they read for real, 25 a day on the sandbox.",
        "value": {
          "external_ref": "ERP-5403",
          "intent": "draft",
          "material": [
            {
              "kind": "text",
              "label": "Kundenmail",
              "content": "Hallo,\n\nanbei die Unterlagen zu meiner Wallbox (Easee Home, 22 kW), Rechnung und Typenschild. Die Anlage laeuft seit 2022, Zaehlernummer 1ESY1161234567. Ich bin Eigentuemer. Bitte Modul 1 nach 14a EnWG anmelden.\n\nViele Gruesse\nThomas Weber\nSchackumer Strasse 20, 40667 Meerbusch"
            },
            {
              "kind": "file",
              "filename": "rechnung-haslinger.pdf",
              "content_type": "application/pdf",
              "data": "JVBERi0xLjQK"
            },
            {
              "kind": "file",
              "filename": "typenschild-wallbox.jpg",
              "content_type": "image/jpeg",
              "data": "/9j/4AAQSkZJRgAB"
            }
          ]
        }
      },
      "CreateLadeeinrichtungDeclared": {
        "summary": "Declared: fields mapped from your CRM, 22 kW, controllable",
        "value": {
          "external_ref": "ERP-5402",
          "intent": "draft",
          "declared": {
            "site": {
              "street": "Schackumer Strasse",
              "house_number": "20",
              "postal_code": "40667",
              "city": "Meerbusch"
            },
            "end_customer": {
              "first_name": "Thomas",
              "last_name": "Weber",
              "email": "t.weber@example.de",
              "phone": "+49 2150 5550144",
              "is_owner": true
            },
            "installer": {
              "company": "Elektro Haslinger GmbH",
              "email": "info@elektro-haslinger.example"
            },
            "device": {
              "count": 1,
              "manufacturer": "Easee",
              "model": "Easee Home",
              "grid_draw_kw": 22,
              "installed_at": "2026-04-19",
              "status": "in_operation"
            },
            "section_14a": {
              "is_controllable": true,
              "control_responsibility": "grid_operator",
              "modules": "1",
              "consent": {
                "given_by": "end_customer",
                "given_at": "2026-04-15T09:30:00Z",
                "channel": "email"
              }
            },
            "connection": {
              "meter_number": "1ESY1161234567",
              "has_smart_meter": false
            }
          }
        }
      },
      "CreateKlimaanlageFixture": {
        "summary": "Fixture: a hosted air conditioning invoice, free, no model call",
        "value": {
          "external_ref": "ERP-5501",
          "intent": "draft",
          "material": [
            {
              "fixture": "air-conditioning-invoice-de"
            }
          ]
        }
      },
      "CreateKlimaanlageRaw": {
        "summary": "Raw: the customer's mail and two files, nothing structured",
        "description": "The shape a back office actually has: a pasted mail and the attachments, no fields. Every value comes back `extracted`, each with the quote it was read from. The two `data` values here are a PDF header and a JPEG header and nothing after: valid base64, sendable, far too little to be documents, so they read as unreadable and the mail carries the case. Send real files and they read for real, 25 a day on the sandbox.",
        "value": {
          "external_ref": "ERP-5503",
          "intent": "draft",
          "material": [
            {
              "kind": "text",
              "label": "Kundenmail",
              "content": "Hallo,\n\nanbei Rechnung und Typenschild zu unserer neuen Klimaanlage (Daikin Perfera FTXM50R, 1,6 kW, Split). Zaehlernummer 1ESY1130077890. Keine kritische Infrastruktur, bitte Modul 1 nach 14a EnWG anmelden.\n\nViele Gruesse\nMaria Brandt\nAm Winterhafen 12, 55131 Mainz"
            },
            {
              "kind": "file",
              "filename": "rechnung-klimatechnik.pdf",
              "content_type": "application/pdf",
              "data": "JVBERi0xLjQK"
            },
            {
              "kind": "file",
              "filename": "typenschild-klimageraet.jpg",
              "content_type": "image/jpeg",
              "data": "/9j/4AAQSkZJRgAB"
            }
          ]
        }
      },
      "CreateKlimaanlageDeclared": {
        "summary": "Declared: fields mapped from your CRM, 1.6 kW, not critical infrastructure",
        "value": {
          "external_ref": "ERP-5502",
          "intent": "draft",
          "declared": {
            "site": {
              "street": "Am Winterhafen",
              "house_number": "12",
              "postal_code": "55131",
              "city": "Mainz"
            },
            "end_customer": {
              "first_name": "Maria",
              "last_name": "Brandt",
              "email": "m.brandt@example.de",
              "phone": "+49 6131 5550166",
              "is_owner": true
            },
            "installer": {
              "company": "Klimatechnik Rhein-Main GmbH",
              "email": "service@klimatechnik-rhein-main.example"
            },
            "device": {
              "count": 1,
              "manufacturer": "Daikin",
              "model": "Perfera FTXM50R",
              "electrical_power_kw": 1.6,
              "starting_current_a": 8.5,
              "grid_draw_kw": 1.6,
              "installed_at": "2026-08-05",
              "status": "planned"
            },
            "section_14a": {
              "is_controllable": true,
              "critical_infrastructure": false,
              "control_responsibility": "metering_operator",
              "modules": "1",
              "consent": {
                "given_by": "end_customer",
                "given_at": "2026-08-01T09:30:00Z",
                "channel": "email"
              }
            },
            "connection": {
              "meter_number": "1ESY1130077890"
            }
          }
        }
      },
      "CreateRawOnly": {
        "summary": "Raw: drop everything, declare nothing",
        "description": "The whole point of the raw channel: an installer's back office has a customer mail and three PDFs and nothing structured at all. Every field in the response is then `source: \"extracted\"`, each carrying the quote it was read from.\n\nWorks on both planes. Reading a document you supply costs a model call per file, which is why the sandbox reads at most 25 real documents per account per day; fixtures are unlimited there.\n\nThe two `data` values are a PDF header and nothing after it: valid base64, so the request is sendable, and far too little to be a document. They carried a trailing `…` until 2026-09-06, which made this example fail validation on encoding the moment anybody pressed Send on it.",
        "value": {
          "external_ref": "ERP-4711",
          "intent": "draft",
          "material": [
            {
              "kind": "text",
              "label": "Kundenmail",
              "content": "Hallo, anbei die Unterlagen zu meiner Wallbox. Die Anlage laeuft seit 2022. Ich bin Eigentuemer. Bitte Modul 1 nach 14a EnWG anmelden.\n\nViele Gruesse\nAlexander Bergmann"
            },
            {
              "kind": "file",
              "filename": "technische-zustimmung.pdf",
              "content_type": "application/pdf",
              "data": "JVBERi0xLjQKJeLjz9MK"
            },
            {
              "kind": "file",
              "filename": "schlussrechnung.pdf",
              "content_type": "application/pdf",
              "data": "JVBERi0xLjQKJcfsj6IK"
            }
          ]
        }
      },
      "CreateDeclaredOnly": {
        "summary": "Declared: declare everything, drop nothing",
        "description": "An ERP that already holds the data. No extraction runs, the case is `ready` on the first call, and `intent: \"file\"` lodges it in one round trip.",
        "value": {
          "external_ref": "ERP-4712",
          "intent": "file",
          "declared": {
            "site": {
              "street": "Ahornweg",
              "house_number": "7a",
              "postal_code": "22607",
              "city": "Hamburg"
            },
            "end_customer": {
              "first_name": "Alexander",
              "last_name": "Bergmann",
              "email": "a.bergmann@example.de",
              "phone": "+49 40 5550123",
              "is_owner": true
            },
            "installer": {
              "company": "Nordlicht Elektrotechnik GmbH",
              "contact_first_name": "Jan",
              "contact_last_name": "Petersen",
              "email": "petersen@example.de",
              "registry_number": "HH-EL-04127"
            },
            "device": {
              "category": "charge_point",
              "manufacturer": "ABL",
              "model": "eMH3 3W2250",
              "count": 1,
              "grid_draw_kw": 11,
              "installed_at": "2022-04-19",
              "status": "in_operation"
            },
            "connection": {
              "meter_number": "1ISK0000123456",
              "has_smart_meter": true
            },
            "section_14a": {
              "modules": "1",
              "control_responsibility": "metering_operator",
              "consent": {
                "given_by": "end_customer",
                "given_at": "2026-08-17T14:05:00Z",
                "channel": "phone",
                "reference": "call-2026-08-17-1405"
              }
            }
          }
        }
      },
      "CreateMixed": {
        "summary": "Mixed: some fields declared, the rest in a PDF",
        "description": "Some fields are known, the rest is in a PDF. Declared values are never overwritten by extraction; where extraction disagrees, the disagreement is reported in `conflicts` instead of being silently resolved.",
        "value": {
          "external_ref": "ERP-4713",
          "intent": "draft",
          "declared": {
            "site": {
              "street": "Ahornweg",
              "house_number": "7a",
              "postal_code": "22607",
              "city": "Hamburg"
            },
            "end_customer": {
              "first_name": "Alexander",
              "last_name": "Bergmann",
              "email": "a.bergmann@example.de"
            },
            "device": {
              "category": "charge_point"
            },
            "section_14a": {
              "modules": "1",
              "consent": {
                "given_by": "end_customer",
                "given_at": "2026-08-17T14:05:00Z",
                "channel": "phone",
                "reference": "call-2026-08-17-1405"
              }
            },
            "nr": {
              "5017": "11"
            }
          },
          "material": [
            {
              "kind": "file",
              "storage_id": "kg21x9…",
              "filename": "schlussrechnung.pdf",
              "content_type": "application/pdf"
            }
          ]
        }
      },
      "PatchResolveConflict": {
        "summary": "Resolve a conflict by declaring the value",
        "description": "Declaring a value settles a conflict, because declared always wins. This is how a caller answers \"the invoice says seven, the operator approved one\".",
        "value": {
          "declared": {
            "device": {
              "count": 1
            }
          }
        }
      },
      "CaseNeedsInput": {
        "summary": "Extraction finished, two things block filing",
        "description": "The realistic outcome of a raw drop. Most fields resolved from the documents, one mandatory field is still open, and the unit count is contradicted between two documents.",
        "value": {
          "id": "anm_7Kq3ZmR8vT",
          "external_ref": "ERP-4711",
          "kind": "section_14a",
          "mode": "live",
          "status": "needs_input",
          "created_at": "2026-08-30T09:12:04Z",
          "updated_at": "2026-08-30T09:12:41Z",
          "operator": {
            "key": "hen",
            "name": "Hamburger Energienetze GmbH"
          },
          "requirements": {
            "surface": "universal",
            "operator_outstanding": 0
          },
          "price": {
            "credits": 1,
            "charged": false
          },
          "fields": [
            {
              "path": "site.street",
              "nr": 1002,
              "label": {
                "de": "Straße",
                "en": "Street"
              },
              "value": "Ahornweg",
              "source": "extracted",
              "confidence": "certain",
              "evidence": {
                "material_id": "mat_7Rb2",
                "filename": "schlussrechnung.pdf",
                "quote": "Ahornweg 7a"
              }
            },
            {
              "path": "site.house_number",
              "nr": 1003,
              "label": {
                "de": "Hausnummer",
                "en": "House number"
              },
              "value": "7a",
              "source": "extracted",
              "confidence": "certain",
              "evidence": {
                "material_id": "mat_7Rb2",
                "filename": "schlussrechnung.pdf",
                "quote": "Ahornweg 7a"
              }
            },
            {
              "path": "site.postal_code",
              "nr": 1007,
              "label": {
                "de": "PLZ",
                "en": "Postal code"
              },
              "value": "22607",
              "source": "extracted",
              "confidence": "certain",
              "evidence": {
                "material_id": "mat_3Kd8",
                "filename": "technische-zustimmung.pdf",
                "quote": "Anlagenadresse: Ahornweg 7a, 22607 Hamburg"
              }
            },
            {
              "path": "site.city",
              "nr": 1008,
              "label": {
                "de": "Ort",
                "en": "City"
              },
              "value": "Hamburg",
              "source": "extracted",
              "confidence": "certain",
              "evidence": {
                "material_id": "mat_3Kd8",
                "filename": "technische-zustimmung.pdf",
                "quote": "Anlagenadresse: Ahornweg 7a, 22607 Hamburg"
              }
            },
            {
              "path": "site.address_kind",
              "nr": 1001,
              "label": {
                "de": "Art der Standortangabe",
                "en": "Kind of location detail"
              },
              "value": "postalische Adresse",
              "source": "derived",
              "confidence": "certain"
            },
            {
              "path": "end_customer.last_name",
              "nr": 1101,
              "label": {
                "de": "Name oder Firmenname",
                "en": "Name or company"
              },
              "value": "Bergmann",
              "source": "extracted",
              "confidence": "certain",
              "evidence": {
                "material_id": "mat_7Rb2",
                "filename": "schlussrechnung.pdf",
                "quote": "Herrn Alexander Bergmann"
              }
            },
            {
              "path": "end_customer.first_name",
              "nr": 1102,
              "label": {
                "de": "Vorname",
                "en": "First name"
              },
              "value": "Alexander",
              "source": "extracted",
              "confidence": "certain",
              "evidence": {
                "material_id": "mat_7Rb2",
                "filename": "schlussrechnung.pdf",
                "quote": "Herrn Alexander Bergmann"
              }
            },
            {
              "path": "installer.company",
              "nr": 1402,
              "label": {
                "de": "Firmenname",
                "en": "Company name"
              },
              "value": "Nordlicht Elektrotechnik GmbH",
              "source": "extracted",
              "confidence": "certain",
              "evidence": {
                "material_id": "mat_7Rb2",
                "filename": "schlussrechnung.pdf",
                "quote": "Nordlicht Elektrotechnik GmbH · Gewerbestraße 12"
              }
            },
            {
              "path": "device.manufacturer",
              "nr": 5002,
              "label": {
                "de": "Hersteller",
                "en": "Manufacturer"
              },
              "value": "ABL",
              "source": "extracted",
              "confidence": "certain",
              "evidence": {
                "material_id": "mat_3Kd8",
                "filename": "technische-zustimmung.pdf",
                "quote": "ABL eMH3 3W2250"
              }
            },
            {
              "path": "device.model",
              "nr": 5003,
              "label": {
                "de": "Typ",
                "en": "Type"
              },
              "value": "eMH3 3W2250",
              "source": "extracted",
              "confidence": "certain",
              "evidence": {
                "material_id": "mat_3Kd8",
                "filename": "technische-zustimmung.pdf",
                "quote": "ABL eMH3 3W2250"
              }
            },
            {
              "path": "device.grid_draw_kw",
              "nr": 5017,
              "label": {
                "de": "Netzbezugsleistung",
                "en": "Grid draw"
              },
              "value": 11,
              "source": "extracted",
              "confidence": "certain",
              "unit": "kW",
              "evidence": {
                "material_id": "mat_3Kd8",
                "filename": "technische-zustimmung.pdf",
                "quote": "Freigegebene Leistung: 11 kW"
              }
            },
            {
              "path": "section_14a.is_controllable",
              "nr": 5010,
              "label": {
                "de": "Steuerbare Verbrauchseinrichtung nach § 14a EnWG",
                "en": "Controllable consumption device under § 14a EnWG"
              },
              "value": "Ja",
              "source": "declared",
              "confidence": "certain"
            },
            {
              "path": "device.installed_at",
              "label": {
                "de": "Inbetriebnahmedatum",
                "en": "Commissioning date"
              },
              "value": "2022-04-19",
              "source": "extracted",
              "confidence": "probable",
              "evidence": {
                "material_id": "mat_7Rb2",
                "filename": "schlussrechnung.pdf",
                "quote": "Liefertermin: 19.04.22"
              }
            }
          ],
          "open": [
            {
              "path": "end_customer.phone",
              "nr": 1109,
              "label": {
                "de": "Telefon",
                "en": "Phone"
              },
              "help": {
                "de": "Tragen Sie die Telefonnummer des Anschlussnutzers ein.",
                "en": "Enter the phone number of the connection user."
              },
              "type": "string",
              "owed_by": "end_customer",
              "blocking": true
            },
            {
              "path": "section_14a.control_responsibility",
              "nr": 2010,
              "label": {
                "de": "Steuerbarkeit",
                "en": "Controllability"
              },
              "help": {
                "de": "Wählen Sie aus, wer mit der Herstellung der Steuerbarkeit beauftragt wird. Hinweis: für die Bereitstellung und Anbindung des Steuergerätes (Steuerbox) an der Übergabestelle Netz zur Kundenanlage ist der Messtellenbetreiber zuständig.",
                "en": "State who is responsible for grid-oriented control."
              },
              "type": "enum",
              "options": [
                {
                  "value": "grid_operator",
                  "label": {
                    "de": "Netzbetreiber",
                    "en": "Grid operator"
                  }
                },
                {
                  "value": "metering_operator",
                  "label": {
                    "de": "Messstellenbetreiber",
                    "en": "Metering point operator"
                  }
                }
              ],
              "owed_by": "us",
              "blocking": true,
              "hint": {
                "de": "Ein intelligentes Messsystem ist vorhanden, daher ist der Messstellenbetreiber wahrscheinlich.",
                "en": "An intelligent metering system is present, so the metering point operator is likely."
              }
            },
            {
              "path": "installer.registry_number",
              "nr": 1411,
              "label": {
                "de": "Eintragungsnummer",
                "en": "Registry number"
              },
              "help": {
                "de": "Tragen Sie die Eintragungsnummer des Elektrofachbetriebs ein.",
                "en": "Enter the registry number of the electrical contractor."
              },
              "type": "string",
              "owed_by": "installer",
              "blocking": false
            }
          ],
          "conflicts": [
            {
              "path": "device.count",
              "nr": 5001,
              "label": {
                "de": "Anzahl Ladeeinrichtungen",
                "en": "Number of charge points"
              },
              "blocking": true,
              "candidates": [
                {
                  "value": 1,
                  "source": "extracted",
                  "evidence": {
                    "material_id": "mat_3Kd8",
                    "filename": "technische-zustimmung.pdf",
                    "quote": "ABL eMH3 3W2250 | 1 | 11 kW | AC"
                  }
                },
                {
                  "value": 7,
                  "source": "extracted",
                  "evidence": {
                    "material_id": "mat_7Rb2",
                    "filename": "schlussrechnung.pdf",
                    "quote": "1 x eMH3 2260 B, 6 x eMH3 2250 B Slave"
                  }
                }
              ],
              "message": {
                "de": "Die Zustimmung des Netzbetreibers nennt eine Ladeeinrichtung, die Rechnung sieben. Bitte bestätigen, wie viele an diesem Netzanschluss betrieben werden.",
                "en": "The grid operator's approval names one charge point, the invoice seven. Please confirm how many run on this connection."
              }
            }
          ],
          "notices": [
            {
              "code": "existing_operator_approval_found",
              "message": {
                "de": "Für diese Anlage liegt bereits eine Zustimmung des Netzbetreibers vor. Ein Netzanschlussbegehren wird deshalb nicht eingereicht.",
                "en": "An approval from the grid operator already exists for this installation, so no connection request will be filed."
              },
              "detail": {
                "operator_reference": "500123",
                "approved_at": "2022-03-11"
              }
            },
            {
              "code": "existing_installation",
              "message": {
                "de": "Die Anlage wurde vor dem 01.01.2024 in Betrieb genommen und wird als Bestandsanlage angemeldet.",
                "en": "The installation was commissioned before 2024-01-01 and is registered as an existing installation."
              },
              "detail": {
                "installed_at": "2022-04-19"
              }
            }
          ],
          "materials": [
            {
              "id": "mat_3Kd8",
              "kind": "file",
              "filename": "technische-zustimmung.pdf",
              "content_type": "application/pdf",
              "bytes": 181012,
              "status": "read",
              "extracted_chars": 5346
            },
            {
              "id": "mat_7Rb2",
              "kind": "file",
              "filename": "schlussrechnung.pdf",
              "content_type": "application/pdf",
              "bytes": 374756,
              "status": "read",
              "extracted_chars": 6236
            },
            {
              "id": "mat_1Qa9",
              "kind": "text",
              "label": "Kundenmail",
              "status": "read",
              "extracted_chars": 168
            }
          ],
          "filings": [],
          "last_event_seq": 2
        }
      },
      "CaseReady": {
        "summary": "Nothing blocking, awaiting the decision to file",
        "value": {
          "id": "anm_7Kq3ZmR8vT",
          "external_ref": "ERP-4711",
          "kind": "section_14a",
          "mode": "live",
          "status": "ready",
          "created_at": "2026-08-30T09:12:04Z",
          "updated_at": "2026-08-30T09:31:10Z",
          "operator": {
            "key": "hen",
            "name": "Hamburger Energienetze GmbH"
          },
          "requirements": {
            "surface": "universal",
            "operator_outstanding": 0
          },
          "price": {
            "credits": 1,
            "charged": false
          },
          "open": [
            {
              "path": "installer.registry_number",
              "nr": 1411,
              "label": {
                "de": "Eintragungsnummer",
                "en": "Registry number"
              },
              "type": "string",
              "owed_by": "installer",
              "blocking": false
            }
          ],
          "conflicts": [],
          "notices": [],
          "filings": [],
          "last_event_seq": 5
        }
      },
      "CaseFiled": {
        "summary": "Filed, with the operator working it",
        "value": {
          "id": "anm_7Kq3ZmR8vT",
          "external_ref": "ERP-4711",
          "kind": "section_14a",
          "mode": "live",
          "status": "submitted",
          "created_at": "2026-08-30T09:12:04Z",
          "updated_at": "2026-08-30T09:33:02Z",
          "operator": {
            "key": "hen",
            "name": "Hamburger Energienetze GmbH"
          },
          "requirements": {
            "surface": "universal",
            "operator_outstanding": 0
          },
          "price": {
            "credits": 1,
            "charged": true,
            "charged_at": "2026-08-30T09:32:55Z"
          },
          "open": [],
          "conflicts": [],
          "notices": [],
          "filings": [
            {
              "id": "flg_5Tz1",
              "kind": "section_14a",
              "mode": "live",
              "stage": "submitted",
              "stage_label": {
                "de": "Beim Netzbetreiber eingereicht",
                "en": "Filed with the grid operator"
              },
              "operator_ref": null,
              "updated_at": "2026-08-30T09:33:02Z"
            }
          ],
          "last_event_seq": 7
        }
      },
      "FieldDictionarySlice": {
        "summary": "A slice of the dictionary",
        "value": {
          "version": "3.0",
          "surface": "universal",
          "operator": null,
          "data": [
            {
              "path": "device.grid_draw_kw",
              "nr": 5017,
              "label": {
                "de": "Netzbezugsleistung",
                "en": "Grid draw"
              },
              "help": {
                "de": "Tragen Sie den Wert für die maximale Netzbezugsleistung der Ladeeinrichtung am Netzanschlusspunkt in Kilowatt (kW) ein.",
                "en": "Enter the maximum grid draw of the charge point."
              },
              "type": "number",
              "unit": "kW",
              "required_when": {
                "device.category": [
                  "charge_point"
                ]
              },
              "owed_by": "installer"
            },
            {
              "path": "section_14a.control_responsibility",
              "nr": 2010,
              "label": {
                "de": "Steuerbarkeit",
                "en": "Controllability"
              },
              "help": {
                "de": "Wählen Sie aus, wer mit der Herstellung der Steuerbarkeit beauftragt wird. Hinweis: für die Bereitstellung und Anbindung des Steuergerätes (Steuerbox) an der Übergabestelle Netz zur Kundenanlage ist der Messtellenbetreiber zuständig.",
                "en": "State who is responsible for grid-oriented control."
              },
              "type": "enum",
              "options": [
                {
                  "value": "grid_operator",
                  "label": {
                    "de": "Netzbetreiber",
                    "en": "Grid operator"
                  }
                },
                {
                  "value": "metering_operator",
                  "label": {
                    "de": "Messstellenbetreiber",
                    "en": "Metering point operator"
                  }
                }
              ],
              "required_when": {
                "device.category": [
                  "charge_point",
                  "heat_pump",
                  "air_conditioning"
                ]
              },
              "owed_by": "us"
            }
          ]
        }
      },
      "ProblemValidation": {
        "summary": "A validation failure",
        "value": {
          "type": "https://wisnots.com/problems/validation-failed",
          "title": "The request could not be accepted",
          "status": 422,
          "code": "validation_failed",
          "detail": "One or more declared values were rejected.",
          "instance": "/v1/anmeldungen/14a",
          "errors": [
            {
              "path": "declared.device.grid_draw_kw",
              "code": "out_of_range",
              "message": "Must be greater than 0 and at most 100."
            },
            {
              "path": "declared.section_14a.modules",
              "code": "invalid_combination",
              "message": "Module 2 cannot be combined with module 1."
            }
          ]
        }
      }
    }
  }
}
