Übersicht
Die PPWR-API ist die vollständige Maschinenschnittstelle für die EPR-Mengenmeldung. Sie besteht aus drei Gruppen, die aufeinander aufbauen:
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. Für PPWR gelten zwei Scopes:
Der Write-Scope schließt den Read-Scope ein — ein Schlüssel mit
packaging:write deckt das gesamte
Integrationsrezept weiter unten ab.
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.Stammdaten-Intake
Endpunkte und Abgleichschlüssel
Anfrageformat
Jeder dieser Endpunkte nimmt entweder ein bloßes Array von Zeilen oder ein Envelope-Objekt:12 und "12", ["plastic"] und
"plastic;paper_board", true und "ja" sind jeweils gleichwertig.
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.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, mit409abgelehnt und es wird nichts geschrieben:Senden Sie den Batch erneut mitoverwrite: trueoder entfernen Sie die betroffenen Zeilen. - Herkunfts- und Nachweisspalten (zuletzt liefernder Lieferant, Zeitstempel, Nachweis-Verweise) sind über diese API niemals beschreibbar.
Einheiten: die Referenz-Falle
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 diegtin 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:
- 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 eigenersizeReference. 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/formatstut 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
- Teil-Akzeptanz: Gültige Zeilen werden übernommen, auch wenn andere in
invalidoderfailedlanden. Einzige Ausnahme ist die409-Überschreibungssperre — die gilt für den ganzen Batch. - Idempotenz: Eine identische Anfrage erneut zu senden ändert nichts; getroffene Zeilen kommen
als
skippedzurück. Ausnahme sind Einheiten-Zeilen ohneinternalRef(siehe oben). importedzählt Entitäten, nicht Zeilen. Eine Größe ist keine eigene Entität: Wird sie mit einer neuen Einheit angelegt, steckt sie in derenimported; wird sie an einer bestehenden Einheit angelegt, erscheint sie inupdatedals<Einheit> / <Größe>. Die Zahl der Größen steht maschinenlesbar insizesCreated.
Fehlerschlüssel
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.Mengen-Feed & Tonnage
Mengen je SKU, Markt und Jahr
Der wiederkehrende Feed: ein Auszug aus Vertrieb/Fakturierung, eine Zeile je SKU × Markt × Jahr.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). 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
articleNumberist ein Zeilenfehler (unknownArticleNumber), niemals ein erfundener Artikel; ein innerhalb eines Batches wiederholter Schlüssel wird beim ersten Vorkommen übernommen, die späteren Zeilen meldenduplicateInFile. 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.
result des Jobs enthält
{ imported, updated, deleted, failed[], derived, cleared, conflicts[], multiLinkedArticleNrs[] }.
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.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.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.
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 (sieheambiguous_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 und senden nur Kategorie-Summen — später auf
SKU-Granularität zu wechseln ist jederzeit möglich, die Vorrangregeln halten beide auseinander.
Mengenmeldung-Berichte
Register und Märkte
Jedesformat 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.
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
reportTypeundsystemOperatorIdsind fürlucid_dePflicht und werden von den Übertragungshilfe-Registern nicht verwendet (diese erzeugen immer den Jahresbericht).reportTypeist die LUCID-Meldeart:HPM1(Planmengenmeldung),HMM1(unterjährige Mengenmeldung),HJM1(Jahresabschlussmengenmeldung),HNM1(Ergänzungsmengenmeldung),HAM1(Abzugsmengenmeldung).systemOperatorIdist die LUCID-Nummer Ihres dualen Systems: zwei Buchstaben und 13 Ziffern (Kleinschreibung wird angenommen und hochgesetzt).marketist ISO-Alpha-2 (Groß-/Kleinschreibung egal),periodYeardas 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:
{ "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:
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.1
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:- Komponenten (
POST /api/ppwr/components) mitmaterialsundmassGrams. - Einheiten (
POST /api/ppwr/units) — die verkaufsfähige Verpackung, verknüpft mit Komponenten überPOST /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 mitformatReference. - SKU-Verknüpfungen —
articleNumbersauf der Einheiten-Zeile oderPOST /api/ppwr/article-links: welcher Artikel in welcher Einheit ausgeliefert wird. Das ist es, was später Verkaufszahlen in Verpackungsmassen übersetzt.
2
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".3
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.4
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.5
Bei LUCID hochladen — der eine Klick, der nicht automatisiert wird
Bei LUCID anmelden, Datenmeldung öffnen, XML hochladen.