> ## Documentation Index
> Fetch the complete documentation index at: https://help.polygon-one.com/llms.txt
> Use this file to discover all available pages before exploring further.

# PPWR: Verpackungsdaten, Mengen, Mengenmeldung

> Die vollständige Maschinenschnittstelle für die EPR-Mengenmeldung — Stammdaten-Intake, Mengen-Feed und Berichtserzeugung

## Übersicht

Die PPWR-API ist die vollständige Maschinenschnittstelle für die EPR-Mengenmeldung. Sie besteht aus
drei Gruppen, die aufeinander aufbauen:

| Gruppe                | Endpunkte                                                  | Zweck                                                                                                    |
| :-------------------- | :--------------------------------------------------------- | :------------------------------------------------------------------------------------------------------- |
| **Stammdaten-Intake** | `components`, `units`, `formats`, `links`, `article-links` | Einmalige Modellierung: Komponenten, Verpackungseinheiten, deren Größen und die Verknüpfungen dazwischen |
| **Mengen & Tonnage**  | `quantities`, `tonnage`                                    | Der wiederkehrende Push aus Vertrieb/Fakturierung                                                        |
| **Mengenmeldung**     | `mengenmeldung/reports`, `mengenmeldung/{reportId}/xml`    | Bericht erzeugen, auflisten, Datei abholen                                                               |

Der Stammdaten-Intake ist ein dünner HTTP-Adapter über **derselben** Upsert-Engine wie der
CSV-/Excel-Import in der App: gleiche Zeilenschemata, gleiche Validierung, gleicher Schreibpfad,
gleiche Idempotenz, gleiche Spaltenpolitik. Was der Datei-Import annimmt, nimmt die API an — mit
identischem Ergebnis.

## Berechtigungen

Schlüsselmechanismus, Basis-URL und die allgemeinen Statuscodes stehen in der
[Einführung](/api-reference/introduction). Für PPWR gelten zwei Scopes:

| Scope             | Gilt für                                               |
| :---------------- | :----------------------------------------------------- |
| `packaging:write` | Alle `POST`-Endpunkte dieser Seite                     |
| `packaging:read`  | Berichtsliste, Datei-Download, Abfrage des Import-Jobs |

Der Write-Scope schließt den Read-Scope ein — ein Schlüssel mit `packaging:write` deckt das gesamte
Integrationsrezept weiter unten ab.

<Info>
  **Modul- und Mandantengrenze.** Alle Endpunkte setzen einen **Kunden-Workspace** mit
  **aktiviertem PPWR-Modul** voraus. Ein Lieferanten- oder Partner-Workspace und ein Konto ohne
  PPWR-Modul erhalten `403`. Jeder Schlüssel ist an genau ein Unternehmen gebunden; er kann Daten
  eines anderen Unternehmens weder lesen noch schreiben.
</Info>

Zusätzlich zu den allgemeinen Statuscodes gibt es diese PPWR-eigenen Fälle:

| Code  | Bedeutung                                                                                                                                 |
| :---- | :---------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | Zeilen- oder Größen-Limit überschritten; bei `POST /api/ppwr/quantities` zusätzlich ein **leerer** Batch (`At least one row is required`) |
| `409` | Unbestätigte Überschreibung (`overwriteRequired`) oder Berichts-Kollision (`reportAlreadyGenerated`)                                      |
| `413` | Mengen-Batch über 5.000 Zeilen (`batchTooLarge`)                                                                                          |
| `422` | Bericht ist noch nicht meldefähig (`blocking_errors` / `nothing_to_declare`)                                                              |

## Stammdaten-Intake

### Endpunkte und Abgleichschlüssel

| Endpunkt                       | Vorgang                                      | Abgleichschlüssel                                            |
| :----------------------------- | :------------------------------------------- | :----------------------------------------------------------- |
| `POST /api/ppwr/components`    | Komponenten anlegen & aktualisieren          | `code` (plus aufgelöster Lieferant)                          |
| `POST /api/ppwr/units`         | Verpackungseinheiten anlegen & aktualisieren | `internalRef` — **optional**, leer heißt immer „neu anlegen" |
| `POST /api/ppwr/formats`       | Größen einer Einheit anlegen & aktualisieren | `specInternalRef` + `reference`                              |
| `POST /api/ppwr/links`         | Verpackungsstückliste anlegen (idempotent)   | `specInternalRef` + `componentCode`                          |
| `POST /api/ppwr/article-links` | Artikelverknüpfungen anlegen (idempotent)    | `specInternalRef` + `articleNumber`                          |

### Anfrageformat

Jeder dieser Endpunkte nimmt **entweder ein bloßes Array von Zeilen** oder ein Envelope-Objekt:

```json theme={null}
{
  "rows": [ { "…": "…" } ],
  "overwrite": false
}
```

Werte werden genauso umgewandelt wie Zellen im Tabellen-Import. `12` und `"12"`, `["plastic"]` und
`"plastic;paper_board"`, `true` und `"ja"` sind jeweils gleichwertig.

<Info>
  **`overwrite` wirkt nur bei Komponenten und Einheiten.** Die Größen-, Stücklisten- und
  Artikelverknüpfungs-Endpunkte akzeptieren das Feld, ignorieren es aber: Größen werden
  „leer heißt behalten" aktualisiert (es gibt nichts zu bestätigen), Verknüpfungen sind
  create-only.
</Info>

### Spaltenpolitik: standardmäßig nur befüllen

* Eine **leere oder weggelassene** Zelle ist ein **No-op** — sie löscht nie einen gespeicherten Wert.
* Eine Zelle, die ein bisher **leeres** Feld befüllt, wird automatisch übernommen.
* Eine Zelle, die einen **vorhandenen** Wert überschreiben würde (auch lieferanten- oder
  KI-attestierte regulatorische Werte), verlangt **`overwrite: true`**. Ohne dieses Flag wird ein
  Batch, der eine solche Änderung enthält, mit `409` abgelehnt und es wird **nichts** geschrieben:

  ```json theme={null}
  { "error": "overwriteRequired", "confirmUpdateCount": 3 }
  ```

  Senden Sie den Batch erneut mit `overwrite: true` oder entfernen Sie die betroffenen Zeilen.
* **Herkunfts- und Nachweisspalten** (zuletzt liefernder Lieferant, Zeitstempel, Nachweis-Verweise)
  sind über diese API **niemals** beschreibbar.

### Einheiten: die Referenz-Falle

<Warning>
  **Eine Einheiten-Zeile ohne `internalRef` ist immer ein Neuanlegen.** Die Referenz *ist* der
  Abgleichschlüssel — eine Zeile, die keine nennt, kann nichts treffen. Der Server vergibt die
  nächste freie Referenz `PU-<n>` Ihres Unternehmens. Wird dieselbe Zeile erneut gesendet, entsteht
  **eine weitere** Einheit.

  Senden Sie deshalb auf jeder Zeile eine `internalRef`, die Sie später wiederverwenden wollen.
  Haben Sie referenzlos importiert, **lesen Sie die vergebenen `PU-n`-Werte zuerst zurück** —
  aktuell nur über die Liste **Verpackungseinheiten** in der App bzw. deren Export, denn es gibt
  noch keinen Lese-Endpunkt dafür. `links` und `article-links` adressieren Einheiten über
  `specInternalRef`.
</Warning>

`notes` ist auf Einheiten-Zeilen **Pflicht** — es ist die „Beschreibung & Verwendungszweck", die auch
das Formular erzwingt. Eine Zeile ohne nicht-leeres `notes` scheitert mit `descriptionRequired`, auch
bei einer reinen Ergänzung einer bestehenden Einheit.

### Größen einer Einheit

Die eigenen Maße und die `gtin` der Einheiten-Zeile sind die **erste (primäre) Größe**. Weitere
Größen senden Sie im optionalen Array `sizes` derselben Zeile — das ist der vorgesehene Weg:

```json theme={null}
{
  "rows": [
    {
      "name": "Teefilter-Faltschachtel", "internalRef": "U-1",
      "packagingType": "sales", "role": "manufacturer",
      "notes": "Faltschachtel für Teefilter",
      "gtin": "4012345678901", "widthMm": 100, "heightMm": 150, "depthMm": 40,
      "sizes": [
        { "reference": "U-1-500", "label": "500 g", "gtin": "4012345678918", "widthMm": 120 },
        { "reference": "U-1-1KG", "label": "1 kg" }
      ]
    }
  ]
}
```

* **Beim Anlegen ist die Zeile eine Transaktion:** Wird eine Größe abgewiesen
  (`duplicateFormatReference`, `formatCapReached`), entsteht auch die Einheit nicht.
* **Trifft die Zeile eine bestehende Einheit**, wird jede Größe für sich aktualisiert; eine
  abgewiesene Größe scheitert allein. Größen, die der Payload nicht nennt, werden **nie** gelöscht.
* Dieselbe Gruppierung geht auch flach: mehrere Zeilen mit derselben `internalRef`, jede mit eigener
  `sizeReference`. So deklariert das CSV-Blatt seine Größen — eine Zelle kann kein Array tragen.
  Beide Schreibweisen dürfen sich in einer Anfrage mischen.
* `POST /api/ppwr/formats` tut dasselbe als eigener Endpunkt, für Aufrufer, die Größen bereits
  getrennt versenden.

### Limits

Höchstens **1.000 Zeilen** pro Anfrage (`400`). Nicht offensichtlich: eine Einheiten-Anfrage trägt
zusätzlich höchstens **1.000 `sizes`-Einträge über alle Zeilen** — eine Zeile darf bis zu 49 Größen
anfordern (eine Einheit trägt höchstens 50, die primäre eingerechnet), das Zeilenlimit allein
begrenzt den Schreibvorgang also nicht. Einen separaten Rate-Limiter gibt es nicht.

### Ergebnis

```json theme={null}
{
  "imported": 2,
  "updated": ["K-3"],
  "skipped": ["K-4"],
  "failed": [{ "identifier": "K-5", "error": "ambiguousComponentMatch" }],
  "invalid": [{ "index": 7, "identifier": "K-6", "error": "materialsRequired" }]
}
```

* **Teil-Akzeptanz:** Gültige Zeilen werden übernommen, auch wenn andere in `invalid` oder `failed`
  landen. Einzige Ausnahme ist die `409`-Überschreibungssperre — die gilt für den ganzen Batch.
* **Idempotenz:** Eine identische Anfrage erneut zu senden ändert nichts; getroffene Zeilen kommen
  als `skipped` zurück. Ausnahme sind Einheiten-Zeilen ohne `internalRef` (siehe oben).
* **`imported` zählt Entitäten, nicht Zeilen.** Eine Größe ist keine eigene Entität: Wird sie mit
  einer neuen Einheit angelegt, steckt sie in deren `imported`; wird sie an einer **bestehenden**
  Einheit angelegt, erscheint sie in `updated` als `<Einheit> / <Größe>`. Die Zahl der Größen steht
  maschinenlesbar in `sizesCreated`.

### Fehlerschlüssel

<Info>
  Die Werte in `failed[]` sind **stabile, nicht lokalisierte Maschinenschlüssel** — alles
  Unerwartete wird auf `dbError` reduziert. In `invalid[]` (Schemaprüfung vor der Engine) ist das
  meistens ebenfalls ein Schlüssel, aber ein Feld ohne eigene Meldung fällt auf den englischen
  Standardtext des Validators zurück. Werten Sie deshalb auf bekannte Schlüssel aus und zeigen Sie
  den Rest unverändert an.
</Info>

Die wichtigsten Schlüssel — u. a.:

| Schlüssel                                                        | Bedeutung                                                                                                                                                                                                                                                                                                             |
| :--------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `materialsRequired`                                              | Komponenten-Zeile ohne Material                                                                                                                                                                                                                                                                                       |
| `descriptionRequired`                                            | Einheiten-Zeile ohne `notes`                                                                                                                                                                                                                                                                                          |
| `internalRefRequired` / `codeRequired` / `articleNumberRequired` | Pflicht-Identifikator fehlt                                                                                                                                                                                                                                                                                           |
| `duplicateInFile`                                                | Derselbe Schlüssel mehrfach in einer Anfrage — das erste Vorkommen gewinnt                                                                                                                                                                                                                                            |
| `ambiguousSupplierName`                                          | Der Lieferantenname passt auf **zwei oder mehr** Ihrer Lieferanten — die Zeile wird abgelehnt, statt zu raten. (Ein Name **ohne** Treffer ist kein Fehler: der Datensatz landet ohne Lieferanten in Ihrem eigenen Bestand.)                                                                                           |
| `ambiguousComponentMatch`                                        | Im Komponenten-Intake: die Zeile nennt **keinen** Lieferanten, aber eine Komponente eines Lieferanten trägt diesen Code bereits — der Import rät nicht zwischen Ihrem eigenen Bestand und dem des Lieferanten. In `componentCodes` und `POST /api/ppwr/links`: der Code benennt **zwei oder mehr** aktive Komponenten |
| `unknownComponentCode`                                           | `POST /api/ppwr/links`: Der Code benennt keine aktive Komponente — hier wird **kein** Platzhalter angelegt                                                                                                                                                                                                            |
| `unknownUnitRef`                                                 | Kein aktives `specInternalRef` in **Ihrem** Unternehmen (eine fremde Referenz löst nie auf)                                                                                                                                                                                                                           |
| `duplicateFormatReference`                                       | Die `reference` gehört bereits zu einer anderen Einheit oder Größe                                                                                                                                                                                                                                                    |
| `formatCapReached`                                               | Die Einheit trägt bereits 50 Größen — bestehende ändern geht weiterhin                                                                                                                                                                                                                                                |
| `conflictingSizeRow`                                             | Eine Größen-Zeile widerspricht den Spalten der Einheit, trägt `componentCodes`, oder eine Einheiten-Zeile trägt `sizeLabel` ohne `sizeReference`                                                                                                                                                                      |
| `unknownFormatRef`                                               | `formatReference` benennt keine Größe dieser Einheit — die Verknüpfung wird **nicht** angelegt                                                                                                                                                                                                                        |
| `duplicateInternalRef` / `duplicateComponentCode`                | Eine gleichzeitig laufende Anfrage hat die Referenz bzw. den Code zwischen Planung und Schreiben belegt — Zeile erneut senden                                                                                                                                                                                         |
| `dbError`                                                        | Unerwarteter Datenbankfehler ohne spezifischen Schlüssel                                                                                                                                                                                                                                                              |

<Warning>
  **Zwei Türen auf dieselbe Verknüpfung verhalten sich unterschiedlich.** Die Spalte
  `articleNumbers` der Einheiten-Zeile **überspringt** eine unbekannte Artikelnummer still (sie
  zählt nur in `articleNumbersUnmatched`), während `POST /api/ppwr/article-links` dieselbe Nummer
  als **Fehlerzeile** meldet. Analog legt `componentCodes` für einen unbekannten Code eine
  Platzhalter-Komponente an, während `POST /api/ppwr/links` `unknownComponentCode` meldet. Bei einem
  **mehrdeutigen** Code scheitern dagegen **beide** Türen mit `ambiguousComponentMatch` — die
  einzige Lösung ist, den Code eindeutig zu machen.
</Warning>

## Mengen-Feed & Tonnage

| Endpunkt                    | Form                                | Abgleichschlüssel                         |
| :-------------------------- | :---------------------------------- | :---------------------------------------- |
| `POST /api/ppwr/quantities` | **Asynchroner Job**, ≤ 5.000 Zeilen | `articleNumber` + `periodYear` + `market` |
| `POST /api/ppwr/tonnage`    | Synchron, ≤ 1.000 Zeilen            | `zsvrCategory` + `periodYear` + `market`  |

### Mengen je SKU, Markt und Jahr

Der wiederkehrende Feed: ein Auszug aus Vertrieb/Fakturierung, eine Zeile je SKU × Markt × Jahr.

```json theme={null}
{
  "rows": [
    { "articleNumber": "SKU-1", "periodYear": 2026, "market": "DE", "quantity": 125000 },
    { "articleNumber": "SKU-1", "periodYear": 2026, "market": "FR", "quantity": 31000 }
  ],
  "replaceScope": "period-market"
}
```

Die Antwort ist `202` mit `{ "jobId": …, "status": "queued", "statusUrl": … }` und einem
`Location`-Header. Fragen Sie anschließend `GET /api/import-jobs/{jobId}` ab (benötigt
`packaging:read`), bis `status` final ist — siehe
[Stapelimport (asynchron)](/api-reference/batch-import). Die Validierung läuft **vor** dem Staging:
Ein fehlerhafter oder leerer Batch ist sofort ein `400` (bzw. `413` über dem 5.000-Zeilen-Limit),
niemals ein Job, der Minuten später scheitert.

* **Korrekturen sind der Normalfall.** Eine Periode mit korrigierten Zahlen erneut zu senden, ist ein
  idempotenter Upsert. Eine unbekannte `articleNumber` ist ein Zeilenfehler
  (`unknownArticleNumber`), niemals ein erfundener Artikel; ein innerhalb eines Batches wiederholter
  Schlüssel wird beim ersten Vorkommen übernommen, die späteren Zeilen melden `duplicateInFile`.
* **`replaceScope: "period-market"`** erklärt den Batch zur maßgeblichen Momentaufnahme jeder
  `(periodYear, market)`-Kombination, die er nennt: Gespeicherte Zeilen genau dieser Kombinationen,
  die der Batch nicht mehr enthält, werden gelöscht. Andere Kombinationen bleiben unberührt. Für
  einen Delta-Feed lassen Sie das Feld weg.
* **Mengen je Einheit werden abgeleitet, nicht gesendet.** Nach jedem angenommenen Batch wird die in
  Verkehr gebrachte Menge jeder Verpackungseinheit als Summe der Mengen ihrer verknüpften SKUs neu
  berechnet. Eine Einheit, die **alle** verknüpften Mengen verliert, bekommt ihren abgeleiteten Wert
  geleert statt veraltet stehen zu lassen.

Das `result` des Jobs enthält
`{ imported, updated, deleted, failed[], derived, cleared, conflicts[], multiLinkedArticleNrs[] }`.

<Info>
  **`multiLinkedArticleNrs` ist ein Ehrlichkeits-Hinweis, kein Fehler.** Es listet die
  **Artikelnummern**, deren Menge in **mehr als eine** Verpackungseinheit fließt — Artikel und
  Einheit stehen absichtlich in einer n:m-Beziehung (z. B. Verkaufs- *und* Transportverpackung),
  also landet dieselbe Menge bei **jeder** verknüpften Einheit. Prüfen Sie diese Artikel auf
  Doppelzählung. Blockierend ist der Hinweis nie.
</Info>

<Warning>
  **Ein von Hand eingetragener Wert wird nie überschrieben** — weder im Mengen-Feed noch bei der
  Tonnage. Betroffene Einheiten kommen im Job-Ergebnis unter `conflicts` mit
  `manualOverridePresent` zurück, betroffene Materialarten in `skipped` der Tonnage-Antwort; die
  übrigen Zeilen werden trotzdem geschrieben. Ein Bestätigen-und-Wiederholen gibt es nicht — ein
  geplanter Push könnte darauf nicht antworten.

  Der Ausweg unterscheidet sich: Eine **Einheit** geben Sie an den Feed zurück, indem Sie ihren Wert
  in der App **leeren** (nur das Leeren — eine neue Zahl zu speichern hält die Einheit manuell).
  Eine **Tonnage-Zeile** kann nicht leer sein, weil `tonnageKg` ein Pflichtwert ist; löschen Sie den
  manuellen Eintrag in der App, dann legt der nächste Push ihn als ERP-eigene Zeile neu an.
</Warning>

<Info>
  **Der Feed schreibt nur auf Einheiten-Ebene.** Eine Verpackungseinheit kann mehrere Größen haben,
  und ein Wert kann eine davon benennen — der Mengen-Feed tut das nie, denn seine Zeilen sind
  SKU-Mengen und sagen nichts darüber aus, welche Größe ausgeliefert wurde. Werte je Größe tragen
  Sie in der App ein; ein Push liest, überschreibt und leert sie nicht. Wiegen die Größen einer
  Einheit eine Komponente unterschiedlich, lässt sich ein Wert auf Einheiten-Ebene nicht in Masse
  umrechnen — der Bericht blockiert dann mit `placement_needs_format`, bis der Wert je Größe
  eingetragen ist.
</Info>

<Note>
  **Die Neuberechnung läuft beim Push**, nicht fortlaufend. Wird eine SKU von einer Einheit gelöst
  oder gelöscht, bleibt deren abgeleiteter Wert bis zum nächsten Push für dieselbe Periode und
  denselben Markt stehen.
</Note>

### Tonnage je Materialart

Der Aggregat-Modus in Kilogramm, für Inverkehrbringer, deren Verpackungszusammensetzung nicht
Einheit für Einheit modelliert ist — und zugleich der Weg, mehrdeutige Materialfamilien zu
deklarieren (siehe `ambiguous_material` weiter unten). **Nur LUCID (Deutschland).**

Zeilen sind `{ periodYear, market, zsvrCategory, tonnageKg, note? }`; die Antwort ist `200` mit
`{ written, skipped, failed, invalid }`. `zsvrCategory` ist eine der acht VerpackG-Materialarten
(`10000` Glas, `20000` Papier/Pappe/Karton, `30000` Eisenmetalle, `40000` Aluminium, `50000`
Kunststoffe, `60000` Getränkekartonverbunde, `70000` sonstige Verbunde, `80000` sonstige
Materialien). `tonnageKg` ist eine **JSON-Zahl** — anders als bei den Stammdaten-Zeilen wird hier
kein Zahlen-String umgewandelt.

Das ist zugleich der vollwertige **Ausweichmodus**: Sind die Daten zur Verpackungszusammensetzung
(noch) zu dünn für eine Stücklisten-Modellierung, überspringen Sie die Schritte 1–3 des
[Integrationsrezepts](#integrationsrezept-erp-zu-lucid) und senden nur Kategorie-Summen — später auf
SKU-Granularität zu wechseln ist jederzeit möglich, die Vorrangregeln halten beide auseinander.

## Mengenmeldung-Berichte

| Endpunkt                                     | Vorgang                     | Scope             |
| :------------------------------------------- | :-------------------------- | :---------------- |
| `POST /api/ppwr/mengenmeldung/reports`       | Bericht erzeugen            | `packaging:write` |
| `GET /api/ppwr/mengenmeldung/reports`        | Erzeugte Berichte auflisten | `packaging:read`  |
| `GET /api/ppwr/mengenmeldung/{reportId}/xml` | Berichtsdatei herunterladen | `packaging:read`  |

### Register und Märkte

Jedes `format` gehört zu genau einem Register und einem Markt — senden Sie den passenden `market`.
Ein abweichender Markt (z. B. `verpact_nl` mit `DE`) wird mit `400` abgelehnt.

| `format`     | Register                  | `market` | Download liefert                        |
| :----------- | :------------------------ | :------- | :-------------------------------------- |
| `lucid_de`   | LUCID (ZSVR, Deutschland) | `DE`     | Datenmeldung als **XML** (Upload-Datei) |
| `verpact_nl` | Verpact (Niederlande)     | `NL`     | Übertragungshilfe (JSON)                |
| `ara_at`     | ARA (Österreich)          | `AT`     | Übertragungshilfe (JSON)                |
| `bdo_pl`     | BDO (Polen)               | `PL`     | Übertragungshilfe (JSON)                |
| `miteco_es`  | MITECO (Spanien)          | `ES`     | Übertragungshilfe (JSON)                |
| `conai_it`   | CONAI (Italien)           | `IT`     | Übertragungshilfe (JSON)                |

Die Nicht-LUCID-Register kennen nur Portalformulare. Ihr Artefakt ist eine deterministische
Übertragungshilfe — die Werte je Kategorie in den Einheiten, Kategorien und der Reihenfolge des
jeweiligen Portals —, die ein Mensch dort abtippt. Mehr kann eine Datei dort nicht automatisieren.

### Bericht erzeugen

```json theme={null}
{
  "periodYear": 2026,
  "market": "DE",
  "format": "lucid_de",
  "reportType": "HJM1",
  "systemOperatorId": "DE1234567890123"
}
```

* `reportType` und `systemOperatorId` sind **für `lucid_de` Pflicht** und werden von den
  Übertragungshilfe-Registern nicht verwendet (diese erzeugen immer den Jahresbericht).
* `reportType` ist die LUCID-Meldeart: `HPM1` (Planmengenmeldung), `HMM1` (unterjährige
  Mengenmeldung), `HJM1` (Jahresabschlussmengenmeldung), `HNM1` (Ergänzungsmengenmeldung),
  `HAM1` (Abzugsmengenmeldung).
* `systemOperatorId` ist die LUCID-Nummer Ihres dualen Systems: zwei Buchstaben und 13 Ziffern
  (Kleinschreibung wird angenommen und hochgesetzt).
* `market` ist ISO-Alpha-2 (Groß-/Kleinschreibung egal), `periodYear` das Kalenderjahr (2000–2100).

**`201`** — `{ "id": "…", "version": 2 }`: Der Bericht wurde erzeugt und eingefroren. Denselben
Bereich nach einer Datenänderung erneut zu erzeugen, ist der Normalfall: Der neue Bericht löst den
vorherigen ab (`version` zählt hoch, die alte Zeile bleibt mit `status: "superseded"` gelistet).

**`409`** — `{ "error": "reportAlreadyGenerated" }`: Zwei Erzeugungen für denselben Bereich liefen
gleichzeitig, eine hat gewonnen. Liste erneut abrufen und bei Bedarf wiederholen.

**`422`** — die Daten sind noch nicht meldefähig; es wurde **nichts** geschrieben:

```json theme={null}
{ "error": "blocking_errors", "blockingErrors": [ { "kind": "missing_mass", "specRef": "U-1" } ] }
```

| `kind`                     | Behebung                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| :------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `missing_mass`             | Eine Komponente einer verknüpften Einheit hat kein Gewicht — Masse nachtragen (API, CSV, Lieferant oder App).                                                                                                                                                                                                                                                                                                                                                                                                         |
| `ambiguous_material`       | Eine Materialart, deren Register-Kategorie sich nicht ableiten lässt (bei LUCID die Metall- und Verbundfamilien). **Für LUCID wird das mit Zahlen gelöst, nicht mit einer Zuordnung:** deklarieren Sie kg für *jede* Kategorie der genannten Familie, je Periode und Markt — in der App oder über `POST /api/ppwr/tonnage` (tragen Sie `0` ein, wo Sie nichts in Verkehr bringen). Bei den Übertragungshilfe-Registern bestätigen Sie die Kategorie der Komponente einmalig in der App; sie wird je Register gemerkt. |
| `ambiguous_multi_material` | Eine Mehrstoff-Komponente: Die Massenaufteilung ist nicht ermittelbar — versuchen Sie **nicht**, sie aufzuteilen. Für LUCID modellieren Sie sie als eine einzige `composite`-Komponente und deklarieren deren kg über die Verbundfamilien-Zahlen. Bei den Übertragungshilfe-Registern ordnen Sie die Komponente in der App einem der `candidates` zu.                                                                                                                                                                 |
| `unmapped_material`        | Nur Übertragungshilfe-Register: eine Materialart ohne Kategorie in der Taxonomie dieses Registers.                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `missing_channel`          | Nur Übertragungshilfe-Register: Der Entsorgungsweg (z. B. Haushalt vs. Gewerbe) wurde für diese Einheit noch nicht gewählt — einmalig je Einheit in der App.                                                                                                                                                                                                                                                                                                                                                          |
| `placement_needs_format`   | Eine Einheit, deren Größen eine Komponente unterschiedlich wiegen, trägt nur einen Wert auf Einheiten-Ebene — tragen Sie ihn je Größe ein.                                                                                                                                                                                                                                                                                                                                                                            |

`{ "error": "nothing_to_declare" }` (mit leerem `blockingErrors`) bedeutet, dass es für diese Periode
und diesen Markt keine meldefähigen Zahlen gibt — bei LUCID weder abgeleitete Mengen noch
Tonnage-Werte, bei den Übertragungshilfe-Registern keine abgeleiteten Mengen (Tonnage-Werte speisen
nur LUCID).

### Berichte auflisten

Optionale Query-Parameter: `periodYear`, `market`, `status` (`generated` oder `superseded`) sowie
`limit` und `offset` — das Paging greift **nach** den Filtern. Ein ungültiger Wert eines bekannten
Parameters ist ein `400`; unbekannte Parameternamen werden ignoriert.

Die Antwort ist ein JSON-Array, neueste zuerst, und enthält ausschließlich Metadaten — nie die
eingefrorene Berechnung:

```json theme={null}
[
  {
    "id": "3f0e…",
    "periodYear": 2026,
    "market": "DE",
    "format": "lucid_de",
    "reportType": "HJM1",
    "systemOperatorId": "DE1234567890123",
    "xsdVersion": "…",
    "status": "generated",
    "version": 2,
    "supersedesId": "9a1b…",
    "snapshotHash": "…",
    "generatedAt": "2026-08-17T09:30:00.000Z"
  }
]
```

Der herunterzuladende Bericht ist der mit `status: "generated"` — genau einer je Kombination aus
`periodYear`, `market` und `format`. Übertragungshilfe-Register melden immer `reportType: "ANNUAL"`,
einen leeren `systemOperatorId` und `xsdVersion: "worksheet-1"`.

### Datei herunterladen

`GET /api/ppwr/mengenmeldung/{reportId}/xml` liefert `200` mit der Datei als Anhang: für `lucid_de`
das LUCID-Datenmeldungs-XML, für die übrigen Register die JSON-Übertragungshilfe. Die Datei wird aus
der eingefrorenen Momentaufnahme **nach erneuter Prüfung ihres SHA-256** neu erzeugt — ein
heruntergeladener Bericht ist damit jedes Mal byte-identisch mit dem erzeugten.

* `404` — unbekannte oder fehlerhafte Berichts-ID, einschließlich jeder ID eines anderen
  Unternehmens.
* `409` — die Integritätsprüfung der Momentaufnahme ist fehlgeschlagen; die Datei wird verweigert
  statt ausgeliefert.

## Integrationsrezept ERP zu LUCID

Ehrlich vorweg: **Schritt 1 ist ein Datenprojekt, alles danach ist Rohrleitung.** Die
Verpackungsstammdaten auf Meldequalität zu bringen, ist echte Arbeit — einmalig, und erneut nur bei
Sortimentsänderungen. Der wiederkehrende Teil (Mengen rein, Bericht raus) ist ein geplanter Job und
zwei HTTP-Aufrufe.

<Steps>
  <Step title="Einmalig: Verpackungsstammdaten">
    Legen Sie zuerst einen API-Schlüssel an; einer mit `packaging:write` deckt alles ab. Das Register
    deklariert **Masse je Materialart**, jede verkaufte SKU muss sich also in Komponenten mit
    monomateriellen Gewichten auflösen:

    1. **Komponenten** (`POST /api/ppwr/components`) mit `materials` und `massGrams`.
    2. **Einheiten** (`POST /api/ppwr/units`) — die verkaufsfähige Verpackung, verknüpft mit
       Komponenten über `POST /api/ppwr/links`. Eine Einheit in mehreren Größen deklariert diese auf
       der Zeile selbst (`sizes[]`); ihre Komponenten-Massen je Größe kommen aus Stücklisten-Zeilen
       mit `formatReference`.
    3. **SKU-Verknüpfungen** — `articleNumbers` auf der Einheiten-Zeile oder
       `POST /api/ppwr/article-links`: welcher Artikel in welcher Einheit ausgeliefert wird. Das ist
       es, was später Verkaufszahlen in Verpackungsmassen übersetzt.

    In der App statt über die API: Hinterlegen Sie Ihre **SystemOperatorID**. Für die
    Übertragungshilfe-Register bestätigen Sie dort außerdem die Kategorie-Zuordnungen, wo eine
    Materialart mehrdeutig ist (einmalig je Register). Für LUCID gibt es nichts zuzuordnen —
    mehrdeutige Familien werden als Zahlen je Periode deklariert.
  </Step>

  <Step title="Wiederkehrend: Mengen je SKU, Markt und Jahr">
    Ein geplanter Export aus Vertrieb/Fakturierung an `POST /api/ppwr/quantities`, danach
    `GET /api/import-jobs/{jobId}` abfragen. Die Frequenz bestimmen Sie: Korrekturen (Retouren,
    Gutschriften) sind ein idempotenter Upsert; wer Vollbestände exportiert, sendet
    `replaceScope: "period-market"`.
  </Step>

  <Step title="Bericht erzeugen">
    `POST /api/ppwr/mengenmeldung/reports` für Periode, Markt und Register. Ein `422` nennt Ihnen
    Position für Position, welche Daten noch nicht meldefähig sind — beheben, erneut pushen, erneut
    erzeugen. Ein `201` ist ein eingefrorener, versionierter Bericht.
  </Step>

  <Step title="Datei abholen">
    `GET /api/ppwr/mengenmeldung/reports?periodYear=2026&market=DE&status=generated`, dann
    `GET /api/ppwr/mengenmeldung/{id}/xml`. Hash-geprüft und reproduzierbar; das LUCID-XML entspricht
    dem veröffentlichten ZSVR-XSD.
  </Step>

  <Step title="Bei LUCID hochladen — der eine Klick, der nicht automatisiert wird">
    Bei LUCID anmelden, **Datenmeldung** öffnen, XML hochladen.

    <Warning>
      Dieser Klick **kann nicht automatisiert werden — von uns so wenig wie von irgendjemandem**:
      Die ZSVR stellt keine Einreichungs-API bereit, und VerpackDG §5(1) S.2 macht Registrierung
      und Datenmeldung zu höchstpersönlichen Pflichten des Inverkehrbringers, die nicht an Dritte
      delegiert werden können. Die Pipeline ist darauf ausgelegt, dass dies der *einzige* manuelle
      Schritt bleibt — und dass er trivial ist: Die Datei ist fertig, validiert und einen
      Browser-Upload von „erledigt" entfernt.
    </Warning>
  </Step>
</Steps>
