Overview
Batch imports run asynchronously: thePOST stages an import job and responds
immediately with 202 and a jobId. Your system then polls the job
(GET /api/v1/import-jobs/{jobId}) until the import finishes and collects the full result
there — including the reasons for skipped or failed rows.
The import runs through the same engine as the in-app CSV importer: same validation,
same idempotent upsert, same result shape.
A
POST with a single object (no rows array) stays synchronous and returns
201 unchanged. Only the batch form (array or envelope) is async.This flow applies to the endpoints with a 5,000-row cap — the three above and
POST /api/v1/ppwr/quantities. The PPWR master-data endpoints and POST /api/v1/ppwr/tonnage
(1,000-row cap; above it 400, not 413) are synchronous: 200 with the result in the
body, no job is created. See PPWR.Flow
-
Send the batch —
POSTa bare array of rows or an envelope object:On success:202with{ "jobId": "…", "status": "queued", "statusUrl": "/api/import-jobs/…" }(also sent as aLocationheader;statusUrlis host-relative — join it with the origin only). Nothing is imported at this point. Row validation runs synchronously — a bad payload is rejected with400right away and no job is staged. The first element ofpathin eachdetailsentry is the 0-based index of the failing row inrows, e.g.[17, "quantity"]. One invalid row rejects the whole request and nothing is staged — fix or drop that row and resend the batch. -
Poll —
GET /api/v1/import-jobs/{jobId}every ~2 seconds, backing off for large jobs (e.g. exponentially up to ~30 s).completedandfailedare terminal — stop polling then. -
Collect the result — once
statusis"completed",resultholds the merged import result (imported,updated,failed,skipped, … per entity). Each entry infailednames its record (orderNumber,internalArticleNrorname) and carrieserror.key— a stable, non-localized key — and sometimeserror.params. Afailedjob carries its reason inerrorinstead (see the table below).
Limits
- Max 5,000 rows per request. Above that:
413 { "error": "batchTooLarge", "maxBatchSize": 5000 }— nothing is staged. Page a larger sync across multiple requests. - ~4.5 MB request body (platform limit, enforced before our application runs).
- For a large initial load, send batches of roughly 100 to 500 orders (each with all its
lines, up to the 5,000-row cap) rather than one request per order: each request becomes one job,
a company’s jobs run only a few at a time, and fewer jobs means fewer
jobIds to poll. - An empty array or a malformed envelope →
400.
Partial accept and idempotency
- Valid rows commit even when other rows land in
failed. - A job writes in chunks. If it fails part-way (
workerError, orlineReplacementRequiredafter a concurrent change), earlier chunks may already be written. Replaying the batch is safe. - Replaying an identical import is safe: matched rows come back as skipped/updated, no duplicates are created.
- Article fields on order rows (
hsCode,articleDescription) are create-only — see Orders.
Error keys (stable, non-localized)
A failed job’serror is a string: either a bare key, or JSON whose key is the stable part.
Row and order keys appear as failed[].error.key in the result of a completed job.
The order-specific keys (
lineReplacementRequired, positionIdMixed, duplicatePositionId,
conflictingHsCodeForArticle, conflictingDuplicateOrderRows) are explained in
Orders. An order key fails the whole order; nothing of it is written.
Retention
The job result stays pollable; the submitted input is deleted ~24 hours after the job finishes, and the job record is deleted ~30 days after creation. Poll and persist the result on your side within that window.Permissions
ThePOST requires the entity’s write permission (articles:write, suppliers:write,
orders:write); polling requires the matching read permission (implied by write). Jobs
are strictly tenant-bound: an unknown jobId and another company’s jobId return the
same 404.