Übersicht
Stapelimporte laufen asynchron: Der POST legt einen Import-Job an und antwortet
sofort mit 202 und einer jobId. Ihr System fragt anschließend den Job-Status ab
(GET /api/import-jobs/{jobId}), bis der Import abgeschlossen ist, und erhält dort das
vollständige Ergebnis — inklusive der Gründe für übersprungene oder fehlgeschlagene Zeilen.
Der Import nutzt dieselbe Engine wie der CSV-Import in der App: gleiche Validierung,
gleiches idempotentes Upsert, gleiches Ergebnisformat.
| Endpunkt | Vorgang | Upsert-Schlüssel |
|---|
POST /api/articles | Artikel im Stapel importieren | internalArticleNr |
POST /api/suppliers | Lieferanten im Stapel importieren | externalSupplierId bzw. Name |
POST /api/orders | Bestellungen im Stapel importieren | orderNumber |
GET /api/import-jobs/{jobId} | Job-Status und Ergebnis abfragen | — |
Ein POST mit einem einzelnen Objekt (ohne rows-Array) bleibt synchron und
antwortet unverändert mit 201. Nur die Stapel-Form (Array bzw. Envelope) ist asynchron.
Ablauf
-
Stapel senden —
POST mit einem Array von Zeilen oder einem Envelope-Objekt:
{ "rows": [ { "…": "…" } ] }
Bei Erfolg: 202 mit { "jobId": "…", "status": "queued", "statusUrl": "/api/import-jobs/…" }
(auch als Location-Header). Zu diesem Zeitpunkt ist noch nichts importiert.
Die Zeilen-Validierung läuft synchron — ein fehlerhafter Payload wird sofort mit 400
abgelehnt, ohne dass ein Job entsteht.
-
Status abfragen —
GET /api/import-jobs/{jobId} etwa alle 2 Sekunden, bei großen
Jobs mit steigendem Abstand (z. B. exponentiell bis ~30 s). completed und failed
sind final — danach nicht weiter abfragen.
-
Ergebnis auswerten — bei
status: "completed" enthält result das
zusammengeführte Import-Ergebnis (imported, updated, failed, skipped, …
je nach Entität). Fehlgeschlagene Zeilen tragen stabile, nicht lokalisierte
Fehlerschlüssel.
Limits
- Maximal 5.000 Zeilen pro Anfrage. Darüber:
413 { "error": "batchTooLarge", "maxBatchSize": 5000 } —
es wird nichts angelegt. Größere Abgleiche auf mehrere Anfragen aufteilen.
- ~4,5 MB Request-Body (Plattform-Limit, greift vor unserer Anwendung).
- Ein leeres Array oder ein fehlerhaftes Envelope →
400.
Teil-Akzeptanz und Idempotenz
- Gültige Zeilen werden übernommen, auch wenn andere Zeilen in
failed landen.
- Ein identischer Import lässt sich gefahrlos wiederholen: bereits vorhandene Zeilen
kommen als übersprungen/aktualisiert zurück, es entstehen keine Duplikate.
Bestellungen: replaceLines und die Vollständigkeitsregel
Der Import einer Bestellung ersetzt deren komplette Positionsliste. Eine Anfrage,
die Bestellung O enthält, muss deshalb alle Positionen von O enthalten. Werden
die Positionen einer Bestellung über mehrere Anfragen verteilt, würde die zweite Anfrage
die zuvor importierten Positionen löschen.
Die Positions-Schutzsperre macht daraus einen lauten Fehler statt eines stillen Datenverlusts:
-
Standard (
replaceLines fehlt oder false): Würde der Stapel gespeicherte Positionen
löschen, schlägt der gesamte Job vor dem ersten Schreibvorgang fehl. Im error-Feld
des Jobs steht:
{ "key": "lineReplacementRequired", "orderNumbers": ["O"] }
Senden Sie die betroffenen Bestellungen mit ihrer vollständigen Positionsliste erneut.
-
"replaceLines": true autorisiert das Ersetzen: Die gesendete Positionsliste wird
maßgeblich, auch wenn dabei Positionen entfallen.
Fehlerschlüssel (stabil, nicht lokalisiert)
| Schlüssel | Bedeutung |
|---|
batchTooLarge | Anfrage überschreitet das 5.000-Zeilen-Limit (HTTP 413) |
lineReplacementRequired | Ein Bestell-Job würde gespeicherte Positionen löschen — vollständige Positionslisten senden oder replaceLines setzen |
dispatchFailed | Job wurde angelegt, aber die Verarbeitung konnte nicht gestartet werden (HTTP 500); es läuft keine Arbeit |
invalidPayload | Die Zeilen sind bei der erneuten Prüfung im Worker durchgefallen; error enthält eine JSON-Zusammenfassung (path + code, nie die Werte selbst) |
workerError | Unerwarteter Fehler nach allen Wiederholungsversuchen (Details serverseitig erfasst) |
dbError | Pro Zeile: unerwarteter Datenbankfehler ohne spezifischen Schlüssel |
Aufbewahrung
Das Job-Ergebnis bleibt abfragbar; der gestagte Eingabe-Payload wird ~24 Stunden nach
Abschluss freigegeben, der Job-Datensatz ~30 Tage nach Erstellung gelöscht. Fragen Sie
das Ergebnis innerhalb dieses Fensters ab und persistieren Sie es auf Ihrer Seite.
Berechtigungen
Der POST erfordert den Write-Scope der Entität (articles:write, suppliers:write,
orders:write), das Abfragen des Jobs den jeweiligen Read-Scope (im Write-Scope enthalten).
Jobs sind strikt mandantengebunden: eine unbekannte jobId und die jobId eines anderen
Unternehmens liefern dieselbe 404.