Skip to main content
You push orders (purchase and sale) with 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 (replaceLines absent or not true): if the batch would delete stored lines, the whole job fails before the first write. The job’s error field 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": true authorizes the replacement: the submitted line set becomes authoritative, even where lines are removed. It must be the JSON boolean true, at the top level next to rows. The string "true" counts as not set, and a bare array of rows cannot set it.
  • Never have the same orderNumber in 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 reached completed or failed.

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 positionId are never merged. Two rows of the same article with different position ids stay two lines; without a positionId they are compared as one line (sum of quantities).
  • All or none per order: within one request, every row of an order must carry a positionId or 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 positionId for 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/orders returns positionId on every line. DDS references are not linked to lines: ddsReferences belongs 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 from lines[]). An article whose HS code still awaits the manual EUDR-relevance check (e.g. heading 9403) keeps the order open, not notRelevant or done; lines[].article.needsManualCheck is true on 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 stays open.
  • open: anything else — including an order with an article that awaits the manual EUDR-relevance check.
A quantity increase, an article change or a withdrawn/rejected DDS turns done back into open — there is no reopen call.
  • Unblocking a position (e.g. an SAP block): release a position only when
    1. the order’s status is done (every EUDR-relevant line is covered by a filed DDS) or notRelevant, or
    2. for a single line, lines[].article.eudrRelevant is false and lines[].article.needsManualCheck is false (the line’s article is not EUDR-relevant).
    A single position has no notRelevant of its own. Always decide on the current read, never on an earlier one. You need ddsReferences only after that decision: for the TRACES reference and the verification number you print on the documents.
Do not release goods because ddsReferences is non-empty. ddsReferences belongs to the whole order and lists every linked DDS that is not withdrawn or rejected — including drafts without a referenceNumber, external and archived statements. When an order carries several references, the API does not say which one covers which position.
  • status can be open while ddsReferences is non-empty. ddsReferences lists 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 order done. Read the TRACES numbers to print from ddsReferences (an external DDS never makes an order done, so its reference is only here); they belong to the order, not to a position. Read status for whether the whole order is covered.
  • ?status= filter: takes exactly one value (open, done or notRelevant) and may be passed only once; anything else is a 400.
  • Not every status flip is a changedSince change for the order. These move status without 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=1000 to reconcile, paging with offset until a page is shorter than limit.
  • 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).
A new copy starts with the template’s HS code and EUDR classification. Without the setting, 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 hsCode keeps 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 whose internalArticleNr is <article nr>#<orderNumber>. An ERP can fill that copy directly, without the supplier portal:
  1. Find the copy. Take it from GET /api/v1/orders → lines[].article (id, and supplierLinks: [{ supplierId, supplierArticleId }]). You can also search GET /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.
  2. Plots. POST /api/v1/plots with supplierArticleIds: [<supplierArticleId>] (plots:write).
  3. Documents. POST /api/v1/evidence/upload-url with { fileName, contentType, fileSize } → PUT the bytes to uploadUrl (same Content-Type, exactly fileSize bytes, otherwise 403, within 10 minutes) → POST /api/v1/evidence as multipart/form-data with supplierId, articleId (the copy’s id), uploadedS3Key (the returned s3Key), uploadedFileName, uploadedFileSize, uploadedMimeType (evidence:write). This path takes files up to 10 MB. The file field instead of the uploaded* fields takes at most 4 MB (a larger file is a 400). List what is stored with GET /api/v1/evidence?supplierArticleId=<id> (evidence:read). To attach the same file to further supplier articles, do not upload it again: send POST /api/v1/evidence per target with only supplierId, articleId and reuseS3Key (the s3Key from the first successful response), optionally with evidenceType or note. 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 supplierArticleId from lines[].article.supplierLinks of 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.