Skip to main content

Overview

Batch imports run asynchronously: the POST stages an import job and responds immediately with 202 and a jobId. Your system then polls the job (GET /api/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.
EndpointOperationUpsert key
POST /api/articlesBulk-import articlesinternalArticleNr
POST /api/suppliersBulk-import suppliersexternalSupplierId or name
POST /api/ordersBulk-import ordersorderNumber
GET /api/import-jobs/{jobId}Poll job status and result
A POST with a single object (no rows array) stays synchronous and returns 201 unchanged. Only the batch form (array or envelope) is async.

Flow

  1. Send the batchPOST a bare array of rows or an envelope object:
    { "rows": [ { "…": "…" } ] }
    
    On success: 202 with { "jobId": "…", "status": "queued", "statusUrl": "/api/import-jobs/…" } (also sent as a Location header). Nothing is imported at this point. Row validation runs synchronously — a bad payload is rejected with 400 right away and no job is staged.
  2. PollGET /api/import-jobs/{jobId} every ~2 seconds, backing off for large jobs (e.g. exponentially up to ~30 s). completed and failed are terminal — stop polling then.
  3. Collect the result — once status is "completed", result holds the merged import result (imported, updated, failed, skipped, … per entity). Failed rows carry stable, non-localized error keys.

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).
  • An empty array or a malformed envelope → 400.

Partial accept and idempotency

  • Valid rows commit even when other rows land in failed.
  • Replaying an identical import is safe: matched rows come back as skipped/updated, no duplicates are created.

Orders: replaceLines and the completeness rule

Importing an order replaces its entire line set. A request that names order O must therefore carry all of O’s lines. If one order’s lines are split across multiple requests, the second request would delete the previously imported lines. The line-replacement gate turns that into a loud failure instead of silent data loss:
  • Default (replaceLines absent or false): if the batch would delete stored lines, the whole job fails before the first write. The job’s error field contains:
    { "key": "lineReplacementRequired", "orderNumbers": ["O"] }
    
    Resend the listed orders with their complete line sets.
  • "replaceLines": true authorizes the replacement: the submitted line set becomes authoritative, even where lines are removed.

Error keys (stable, non-localized)

KeyMeaning
batchTooLargerequest exceeded the 5,000-row cap (HTTP 413)
lineReplacementRequiredan orders job would delete stored lines — resend full line sets or set replaceLines
dispatchFailedthe job was staged but processing could not be started (HTTP 500); no work will run
invalidPayloadthe rows failed re-validation in the worker; error holds a JSON summary (path + code, never the values themselves)
workerErrorunexpected error after all retries (details captured server-side)
dbErrorper row: an unexpected database failure with no specific key

Retention

The job result stays pollable; the staged input payload is freed ~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

The POST requires the entity’s write scope (articles:write, suppliers:write, orders:write); polling requires the matching read scope (implied by write). Jobs are strictly tenant-bound: an unknown jobId and another company’s jobId return the same 404.