POST /api/v1/orders and read them back with
GET /api/v1/orders — keyed by your own orderNumber and hydrated with the linked DDS
(ddsReferences). In a batch import each row is one order line; rows with the same orderNumber
form one order, which upserts idempotently on orderNumber. This page covers the rules that apply
to orders only. The generic flow — stage a job, poll its status, collect the result — is in
Batch Import (async).
An order’s supplier, orderType and orderDate are fixed once the order is stored. A push that
changes any of them fails that order with conflictingDuplicateOrderRows. Rows of one order that
disagree on an order-level field (supplier fields, orderType, orderDate, deliveryDate, DDS
reference fields) fail the same way. A changed deliveryDate on an existing order is applied.
A POST with a single object creates one new order. If that orderNumber already exists, the
answer is 409. Before you resend a single order, look it up with
GET /api/v1/orders?orderNumbers=<nr>, or send it as a one-row batch, which is safe to replay. The single-object
form stores lines[].positionId; mixed or repeated position ids answer 400 with code
(positionIdMixed, duplicatePositionId). PATCH /api/v1/orders/{id} answers the same way, and a
PATCH that changes the order number to one that already exists answers 409.
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 check turns that into a loud failure instead of silent data loss:
-
Default (
replaceLinesabsent or nottrue): if the batch would delete stored lines, the whole job fails before the first write. The job’serrorfield is a string that holds JSON — parse it:Resend the listed orders with their complete line sets. One exception: if another write changes one of the orders while the job runs, the job can fail with this key after earlier orders of the same job were already written. -
"replaceLines": trueauthorizes the replacement: the submitted line set becomes authoritative, even where lines are removed. It must be the JSON booleantrue, at the top level next torows. The string"true"counts as not set, and a bare array of rows cannot set it. -
Never have the same
orderNumberin two requests in flight at the same time: a company’s jobs may run concurrently and their order is not guaranteed, so the stored lines could end up being the older request’s. Send a changed order again only after the previous job for it has reachedcompletedorfailed.
Positions (positionId)
Every row of a batch import may carry an optional positionId — the line’s position number in
your ERP (e.g. the SAP purchase-order item 00010). It is trimmed and then compared character by
character (not numerically, leading zeros are kept), at most 64 characters.
- Rows with a
positionIdare never merged. Two rows of the same article with different position ids stay two lines; without apositionIdthey are compared as one line (sum of quantities). - All or none per order: within one request, every row of an order must carry a
positionIdor none of them (positionIdMixed). A position id may appear only once per order (duplicatePositionId). Either fails the whole order; nothing of it is written. - Matching: when an order is sent again, each line is compared by its
positionId— a changed quantity or a changed article on the same position id counts as a change, a missing position id as a removed line (line-replacement check, see above). - Switching: an order stored without position ids that is sent with
positionIdfor the first time counts as fully replaced. The same holds the other way round: an order stored with position ids that is sent without them. Send that import with"replaceLines": true. - Read-back:
GET /api/v1/ordersreturnspositionIdon every line. DDS references are not linked to lines:ddsReferencesbelongs to the whole order (see below).
Order status (status)
GET /api/v1/orders returns a status field on every order with one of three values. It is
derived on every read, never stored:
notRelevant: no line of the order carries an EUDR-relevant article. Lines whose article was deleted do not count (they also disappear fromlines[]). An article whose HS code still awaits the manual EUDR-relevance check (e.g. heading 9403) keeps the orderopen, notnotRelevantordone;lines[].article.needsManualCheckistrueon that line.done: every EUDR-relevant line is covered by a submitted, validated or grouped DDS linked to this order that declares the line’s article for the order’s supplier with enough volume. A DDS entry is a supplier article: it covers only orders of that same supplier and is consumed only by their quantities. The article’s quantity summed over every order of that supplier linked to the DDS must not exceed the declared volume — exactly like the DDS’s own remaining volume. An order of another supplier linked to that DDS by hand staysopen.open: anything else — including an order with an article that awaits the manual EUDR-relevance check.
done back into
open — there is no reopen call.
-
Unblocking a position (e.g. an SAP block): release a position only when
- the order’s
statusisdone(every EUDR-relevant line is covered by a filed DDS) ornotRelevant, or - for a single line,
lines[].article.eudrRelevantisfalseandlines[].article.needsManualCheckisfalse(the line’s article is not EUDR-relevant).
notRelevantof its own. Always decide on the current read, never on an earlier one. You needddsReferencesonly after that decision: for the TRACES reference and the verification number you print on the documents. - the order’s
statuscan beopenwhileddsReferencesis non-empty.ddsReferenceslists every linked DDS that is not withdrawn or rejected — including drafts, archived, external, under-control and soft-deleted statements, and statements whose declared volume is over-assigned; none of those make the orderdone. Read the TRACES numbers to print fromddsReferences(an external DDS never makes an orderdone, so its reference is only here); they belong to the order, not to a position. Readstatusfor whether the whole order is covered.?status=filter: takes exactly one value (open,doneornotRelevant) and may be passed only once; anything else is a400.- Not every
statusflip is achangedSincechange for the order. These movestatuswithout moving the order: another order raising its quantity on the same DDS, an article’s EUDR relevance changing, an article’s manual check being resolved, an article on the order being deleted, a DDS article’s declared volume being edited, a supplier article being deleted, a covering DDS’s articles changing. Re-read with?status=open&limit=1000to reconcile, paging withoffsetuntil a page is shorter thanlimit. - Deleting a supplier removes all its orders from
GET /api/v1/orders, full syncs included. Treat an order that is missing from a full sync as deleted.
Delta sync (changedSince)
Poll GET /api/v1/orders?changedSince=<start of your previous poll minus a few minutes>&limit=1000
(hourly recommended). changedSince is an ISO 8601 timestamp with a time zone (Z or an offset,
e.g. 2026-10-05T05:55:00Z), passed once; anything else is a 400.
A full page (1000 orders) means changed orders were left out. Page the same changedSince window
with offset until a page is shorter than limit, and poll more often. An order can appear on two
pages, and an order that changes while you page shows up again in the next delta. An order deleted
while you page can shift the pages so that one order is skipped — the nightly full sync catches it.
There is no changedBefore; a nightly full sync is the recovery for anything a delta missed:
deleted orders, orders of a deleted supplier and unlinked DDS never show up in a delta.
Article fields on order rows are create-only
hsCode and articleDescription
are read only when articleNumber matches no article at all — the row then creates
it (counted in autoCreated.articles; a blank or placeholder hsCode such as - fails
the whole order with articleMissingHsCode; a deleted article’s number fails it with
articleDeleted). For an existing article both fields are silently ignored (no
result flag) — the article’s HS code and EUDR relevance stay unchanged. Change article
master data via PATCH /api/v1/articles/{id} or the articles batch import. On an article with
per-order copies (see below), that changes the template only: the copies keep their HS code, and
they take the new EUDR classification only if their HS code equals the template’s and their order
has no linked DDS (other than withdrawn or rejected ones).
HS code on order rows with per-order article copies (hsCode)
An order line can point at a per-order copy of an article instead of the article itself. The
article is then a template, and each order gets one copy per article (two lines of the same
article share it); the copy’s internalArticleNr is <article nr>#<orderNumber>. An article is a
template when:
- a purchase-order import created it — in every workspace, with or without the setting below;
- it was sent with
sourceScope: "perOrder"; - the workspace setting “order import creates per-order copies” is on (off by default, switched on by Polygon One on request) and a purchase-order row names it — except an article that carries an external article number (articles created by the order import carry none).
hsCode on a known article number stays create-only (see above). With the setting, hsCode on a
purchase-order row works as follows:
- The copy carries the row’s HS code. EUDR relevance is recomputed when the HS code differs
from the template’s; otherwise the template’s classification is kept. A row without
hsCodekeeps the copy it has, or takes the template’s HS code. - A changed HS code moves the line to another version. Re-pushing the order with a
changed HS code (compared without separators, so
4407.11=440711) moves the line to the version of that order’s copy that has this HS code. A new version is created only if none exists yet; an older version with that HS code is reused, together with its supplier data. A new version starts without supplier data. The other versions stay untouched. - Asking the supplier again. The auto-invite automation (
automations.autoInviteSuppliers) invites with its own rules: a supplier in an EU country is not invited; a supplier outside the EU is invited whatever the risk level of its country, as long as EUDR-relevant data is still missing; a supplier without an email address is skipped (you get a notification); a live invite that you deliver manually is widened to the new version but not emailed. If per-order origin confirmation (automations.confirmOriginPerOrder) is switched on and the supplier holds a live invite, the supplier can also get an origin-confirmation request. That request is keyed by the article, not by the HS code, so it can offer “origin unchanged” for the new version, based on an earlier answered copy. - Unchanged HS code: an identical re-push changes nothing — no new version, no new request.
- Conflicting HS codes: two rows of one order naming one article with different HS codes —
or a concurrent push that created the same version with another HS code — fail the order with
conflictingHsCodeForArticle.
Per-order origin via ERP
With per-order copies (see above), each order line points at its own copy of the article whoseinternalArticleNr is <article nr>#<orderNumber>. An ERP can fill that copy directly, without the
supplier portal:
-
Find the copy. Take it from
GET /api/v1/orders→lines[].article(id, andsupplierLinks: [{ supplierId, supplierArticleId }]). You can also searchGET /api/v1/articles?internalArticleNr=<nr>%23<orderNumber>(exact match;#encoded as%23); after an HS change this returns one article per version, so take the one the order line points at. -
Plots.
POST /api/v1/plotswithsupplierArticleIds: [<supplierArticleId>](plots:write). -
Documents.
POST /api/v1/evidence/upload-urlwith{ fileName, contentType, fileSize }→PUTthe bytes touploadUrl(sameContent-Type, exactlyfileSizebytes, otherwise403, within 10 minutes) →POST /api/v1/evidenceasmultipart/form-datawithsupplierId,articleId(the copy’s id),uploadedS3Key(the returneds3Key),uploadedFileName,uploadedFileSize,uploadedMimeType(evidence:write). This path takes files up to 10 MB. Thefilefield instead of theuploaded*fields takes at most 4 MB (a larger file is a400). List what is stored withGET /api/v1/evidence?supplierArticleId=<id>(evidence:read). To attach the same file to further supplier articles, do not upload it again: sendPOST /api/v1/evidenceper target with onlysupplierId,articleIdandreuseS3Key(thes3Keyfrom the first successful response), optionally withevidenceTypeornote. Each call creates its own evidence record on that article.
Under v1 only
POST /api/v1/evidence/upload-url, POST /api/v1/evidence and GET /api/v1/evidence
are part of the contract. Deleting or re-analysing evidence is not available to API keys (403;
the body is { "error": "Not available for API keys" }, or { "error": "Forbidden" } when the key
lacks the route’s permission).
The ERP integration key preset includes plots:write and evidence:write. A key created with
this preset before 1 October 2026 lacks them: check the key’s permissions, or create a new key.
A document pushed this way is buyer-authored evidence (recorded as uploaded by the API key’s user); a
supplier’s own upload through the portal carries stronger provenance.
Have the supplier confirm the origin
Once all current plots (and documents) of an order are uploaded, ask the supplier to confirm that origin data:- Selection: each
supplierArticleIdfromlines[].article.supplierLinksof this order — a live link of an EUDR-relevant article on the order to its supplier. Validation is all-or-nothing: if one id is invalid, nothing is queued. - Response:
202 { "status": "queued", "orderId", "supplierArticleCount" }. The supplier request and its email are created asynchronously. Repeating the call merges into the active request and sends no further email. - What the supplier confirms: this order’s current origin data. Previous-order data is never copied, and the authorship of the data you uploaded stays unchanged.
- Requirements:
orders:write, and per-order origin confirmation (automations.confirmOriginPerOrder) switched on for your workspace.