Skip to main content

Ü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.
Zusätzlich zu den allgemeinen Statuscodes gibt es diese PPWR-eigenen Fälle:

Stammdaten-Intake

Endpunkte und Abgleichschlüssel

Anfrageformat

Jeder dieser Endpunkte nimmt entweder ein bloßes Array von Zeilen oder ein Envelope-Objekt:
Werte werden genauso umgewandelt wie Zellen im Tabellen-Import. 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, mit 409 abgelehnt und es wird nichts geschrieben:
    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

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.
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:
  • 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

  • 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

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.
Die wichtigsten Schlüssel — u. a.:
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.

Mengen-Feed & Tonnage

Mengen je SKU, Markt und Jahr

Der wiederkehrende Feed: ein Auszug aus Vertrieb/Fakturierung, eine Zeile je SKU × Markt × Jahr.
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). 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[] }.
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.
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.
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 (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 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

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. 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

  • 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:
{ "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:
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.
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:
  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üpfungenarticleNumbers 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.
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.
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.