> ## Documentation Index
> Fetch the complete documentation index at: https://help.polygon-one.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Recommended integration flows

> The end-to-end sequences we recommend for an ERP integration — order sync, plots and geo analysis, PPWR volume reporting

The other pages document every endpoint and rule. This page puts them in order: which calls an ERP
integration makes, in which sequence, and what it should store. Each flow links to the page with the
details. Base URL, key and headers are as in the [Quickstart](https://help.polygon-one.com/en/api-reference/quickstart.md).

| Flow | Module | Use it when |
| :- | :- | :- |
| [Order sync](#order-sync) | EUDR | Your ERP holds purchase or sale orders and needs the TRACES reference per order position back |
| [Plots and geo analysis](#plots-and-geo-analysis) | EUDR | You have plot geometries and want the deforestation analysis and the plot report |
| [PPWR volume reporting](#ppwr-volume-reporting) | PPWR | You report packaging volumes to LUCID or another national register |

## Order sync

ERP to Polygon One, and the DDS references back — the main EUDR flow. Your ERP pushes master data
and orders; in Polygon One the orders are linked to due diligence statements (DDS); your ERP reads
back, per order and position, whether the order is covered and which TRACES reference belongs to it.

**1. Push master data first: suppliers, then articles**

Send suppliers and articles as batches before the orders, so that order rows resolve to articles
that already exist — `hsCode` and `articleDescription` on an order row are only read when the
article does not exist yet ([create-only](https://help.polygon-one.com/en/api-reference/orders.md#article-fields-on-order-rows-are-create-only)).
Suppliers are upserted on `externalSupplierId` or name, articles on `internalArticleNr`. A purchase
article needs a supplier (`supplierId` or `supplierName`).

```bash theme={null}
curl -X POST "https://app.polygon-one.com/api/v1/suppliers" \
  -H "Authorization: Bearer eudr_8f7d2a..." \
  -H "Content-Type: application/json" \
  -d '{ "rows": [ { "name": "Sawmill Example GmbH", "externalSupplierId": "LIEF-4711", "email": "eudr@sawmill-example.com", "country": "Germany", "address": "Waldweg 1, 79100 Freiburg" } ] }'
```

```bash theme={null}
curl -X POST "https://app.polygon-one.com/api/v1/articles" \
  -H "Authorization: Bearer eudr_8f7d2a..." \
  -H "Content-Type: application/json" \
  -d '{ "rows": [ { "name": "Pine plank 20 mm", "internalArticleNr": "ART-1001", "hsCode": "4407.11", "supplierName": "Sawmill Example GmbH" } ] }'
```

Each answers `202` with a `jobId`. Wait until the supplier job has finished before you send the
articles, and until the article job has finished before you send the orders (poll as in
step 3). Jobs of one workspace can run at the same time: an article row whose `supplierName` is
not stored yet creates a new supplier with only that name.

**2. Push orders as a batch — every line of an order in the same request**

One row is one order line; rows with the same `orderNumber` form one order. Importing an order
replaces its whole line set, so a request that names an order must carry **all** of its lines. If
your ERP has position numbers, send them as `positionId` on every row of the order.

```bash theme={null}
curl -X POST "https://app.polygon-one.com/api/v1/orders" \
  -H "Authorization: Bearer eudr_8f7d2a..." \
  -H "Content-Type: application/json" \
  -d '{
    "rows": [
      { "orderNumber": "PO-4500012345", "orderType": "purchase", "supplierName": "Sawmill Example GmbH", "articleNumber": "ART-1001", "positionId": "00010", "quantity": 1200 },
      { "orderNumber": "PO-4500012345", "orderType": "purchase", "supplierName": "Sawmill Example GmbH", "articleNumber": "ART-1001", "positionId": "00020", "quantity": 800 }
    ]
  }'
```

`quantity` is the net mass in kilograms. At most 5,000 rows per request — page a larger sync
across several requests, keeping each order's lines together.

**3. Poll the import job and store the result**

```bash theme={null}
curl "https://app.polygon-one.com/api/v1/import-jobs/6f7a..." \
  -H "Authorization: Bearer eudr_8f7d2a..."
```

Poll about every 2 seconds, backing off for large jobs (e.g. exponentially up to \~30 s), until
`status` is `completed` or `failed`. Persist `result` on your side — the job record is deleted
\~30 days after creation. Failed rows carry stable error keys in `failed[].error.key`. A
`failed` job carries its reason in `error`, a string; for `lineReplacementRequired` it holds
JSON with the `orderNumbers` — a request would have deleted stored lines: resend the listed
orders with their complete line sets.

**4. Delta-poll the orders on a schedule**

Poll `changedSince` = the start of your previous poll minus a few minutes (an ISO 8601
timestamp with `Z` or an offset), with `limit=1000`. Hourly is recommended. `fields` keeps the
response to what the decision in step 5 needs.

```bash theme={null}
curl "https://app.polygon-one.com/api/v1/orders?changedSince=2026-10-05T05:55:00Z&limit=1000&fields=orderNumber,status,lines,ddsReferences" \
  -H "Authorization: Bearer eudr_8f7d2a..."
```

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 twice; one that changes while you page comes back in the next delta.

**5. Decide per order and position**

Decide on what the **current** read returns, never on an earlier one:

* **Whether the whole order is covered:** `status` (`done`, `open` or `notRelevant`). An
  article awaiting the manual EUDR-relevance check keeps the order `open`.
* **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).
* **Which TRACES reference to print:** only after that decision, from `ddsReferences`
  (`referenceNumber`, `verificationNumber`), for the documents.

> [!WARNING]
>
> 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 (e.g. a draft or an external DDS). A
withdrawn or rejected DDS drops out of `ddsReferences`, and the order comes back in the next delta.

**6. Run a nightly full sync as recovery**

Deleted orders, orders of a deleted supplier and unlinked DDS never show up in a delta. Once a
night, read the full list and reconcile, paging with `offset` until a page is shorter than
`limit`. Treat an order missing from the full list as deleted.

```bash theme={null}
curl "https://app.polygon-one.com/api/v1/orders?limit=1000&offset=0&fields=orderNumber,status,lines,ddsReferences" \
  -H "Authorization: Bearer eudr_8f7d2a..."
```

Some `status` changes do not move the order into a delta (e.g. another order raising its quantity
on the same DDS, or an article's manual check being resolved). Re-read with `?status=open&limit=1000` to reconcile them, paging with `offset`
the same way.

**What goes wrong most often**

* **Splitting an order across requests.** The second request would delete the lines of the first;
  the check fails the whole job with `lineReplacementRequired` instead. Send complete line sets, or
  `"replaceLines": true` when the line set is meant to shrink.
* **`positionId` on some rows only.** All rows of an order carry one, or none (`positionIdMixed`),
  and each only once per order (`duplicatePositionId`). An order stored without position ids that
  is sent with them for the first time needs `"replaceLines": true`.
* **Releasing goods on a reference.** Release only on `status` or on `lines[].article.eudrRelevant`
  and `lines[].article.needsManualCheck` both `false` in the current read — never because `ddsReferences` is non-empty, and never on an earlier read.
* **Stopping at a full delta page.** The orders beyond it are not returned again unless they
  change; page the window with `offset`.

Details: [Orders](https://help.polygon-one.com/en/api-reference/orders.md) ([completeness rule](https://help.polygon-one.com/en/api-reference/orders.md#orders:-replacelines-and-the-completeness-rule),
[positions](https://help.polygon-one.com/en/api-reference/orders.md#positions-positionid), [status](https://help.polygon-one.com/en/api-reference/orders.md#order-status-status),
[delta sync](https://help.polygon-one.com/en/api-reference/orders.md#delta-sync-changedsince)) and
[Batch Import (async)](https://help.polygon-one.com/en/api-reference/batch-import.md).

### Variant: per-order origin from the ERP

An order line points at its own copy of the article when the article is a per-order template: an
article a purchase-order import created, an article sent with `sourceScope: "perOrder"`, or — with
the workspace setting **"order import creates per-order copies"** (off by default, switched on by
Polygon One on request) — an existing article a purchase-order row names. Your ERP can attach plots
and documents to that copy directly, without the supplier portal: find the copy, create plots with
its `supplierArticleId`, upload evidence. This needs `plots:write` and `evidence:write`; the **ERP
integration** key preset includes both (a key created with it before 1 October 2026 lacks them —
check its permissions). Steps and responses:
[Per-order origin via ERP](https://help.polygon-one.com/en/api-reference/orders.md#per-order-origin-via-erp).

Once all current plots are uploaded, call `POST /api/v1/orders/{id}/origin-confirmation` with the
selected order-line `supplierArticleIds`; the supplier then confirms this order's current origin
data. Previous-order data is never copied, and buyer authorship stays unchanged. Details:
[Have the supplier confirm the origin](https://help.polygon-one.com/en/api-reference/orders.md#have-the-supplier-confirm-the-origin).

## Plots and geo analysis

Send a plot geometry, wait for the deforestation analysis, read the result, download the report. No
article or supplier is needed — a plot with a commodity group is enough.

**1. Optional: check the geometry**

A dry run with the same body as create — writes nothing, answers `valid` with the codes create
would answer. `commodityGroup` is optional here, but if you send `"wood"` the dry run also
requires a scientific species, as create does. It does not check your plot limit.

```bash theme={null}
curl -X POST "https://app.polygon-one.com/api/v1/plots/validate" \
  -H "Authorization: Bearer eudr_8f7d2a..." \
  -H "Content-Type: application/json" \
  -d '{ "name": "LIEF-4711-P03", "country": "BR", "commodityGroup": "wood", "species": [{ "commonName": "Eucalyptus", "scientificName": "Eucalyptus grandis" }], "geojson": { "type": "Polygon", "coordinates": [[[-47.060123, -21.180245], [-47.058531, -21.180245], [-47.058531, -21.181934], [-47.060123, -21.181934], [-47.060123, -21.180245]]] } }'
```

**2. Create the plot**

```bash theme={null}
curl -X POST "https://app.polygon-one.com/api/v1/plots" \
  -H "Authorization: Bearer eudr_8f7d2a..." \
  -H "Content-Type: application/json" \
  -d '{ "name": "LIEF-4711-P03", "country": "BR", "commodityGroup": "wood", "species": [{ "commonName": "Eucalyptus", "scientificName": "Eucalyptus grandis" }], "geojson": { "type": "Polygon", "coordinates": [[[-47.060123, -21.180245], [-47.058531, -21.180245], [-47.058531, -21.181934], [-47.060123, -21.181934], [-47.060123, -21.180245]]] } }'
```

`201` with the plot under `created`. Take `created.id`. `409` with
`code: "PLOT_LIMIT_REACHED"` means your plot limit or monthly quota is reached: nothing is
created, and a retry does not help.

**3. Poll the analysis status**

Every 15 to 30 seconds while `analysisStatus` is `pending` or `in_progress`:

```bash theme={null}
curl "https://app.polygon-one.com/api/v1/plots/5b0c…?fields=analysisStatus,analysis" \
  -H "Authorization: Bearer eudr_8f7d2a..."
```

**4. Download the report once**

```bash theme={null}
curl "https://app.polygon-one.com/api/v1/plots/5b0c…/report" \
  -H "Authorization: Bearer eudr_8f7d2a..." \
  -o LIEF-4711-P03.pdf
```

**What goes wrong most often**

* **Sending a `FeatureCollection`.** One request creates one plot — send each feature on its own.
* **Polling the report instead of the status.** Every report call renders the PDF anew; download it
  once, when the result exists.
* **No plot reference in `name`.** Same `name`, `country`, geometry and `supplierArticleIds` again is
  recognised (`"deduplicated": true`) — that is what makes a retry safe.
* **No `country`.** Without it, the low-risk skip never applies and the plot is always analyzed.

Details: [Geo analysis](https://help.polygon-one.com/en/api-reference/geo-analysis.md) ([status](https://help.polygon-one.com/en/api-reference/geo-analysis.md#polling-the-status),
[idempotency](https://help.polygon-one.com/en/api-reference/geo-analysis.md#idempotency), [error codes](https://help.polygon-one.com/en/api-reference/geo-analysis.md#error-codes),
[recipe](https://help.polygon-one.com/en/api-reference/geo-analysis.md#recipe:-from-geojson-to-report)).

## PPWR volume reporting

A short form of the [Integration recipe ERP to LUCID](https://help.polygon-one.com/en/api-reference/ppwr.md#integration-recipe-erp-to-lucid).
Steps 1–3 are one-time master data; steps 4–6 are the recurring job. The master-data endpoints answer
synchronously (`200`); the quantity feed is an import job. The articles (SKUs) must already exist in
Polygon One: create missing ones with `POST /api/v1/articles` (`articles:write`). An unknown article
number is an error row in `article-links` and in the quantity job.

**1. Components with material and mass**

```bash theme={null}
curl -X POST "https://app.polygon-one.com/api/v1/ppwr/components" \
  -H "Authorization: Bearer eudr_8f7d2a..." \
  -H "Content-Type: application/json" \
  -d '{ "rows": [ { "code": "K-1", "name": "Folding box", "materials": ["paper_board"], "massGrams": "12" } ] }'
```

**2. Packaging units — always with an internalRef**

A unit row without `internalRef` always creates a new unit. `notes` is mandatory.

```bash theme={null}
curl -X POST "https://app.polygon-one.com/api/v1/ppwr/units" \
  -H "Authorization: Bearer eudr_8f7d2a..." \
  -H "Content-Type: application/json" \
  -d '{ "rows": [ { "name": "Tea filter folding box", "internalRef": "U-1", "packagingType": "sales", "role": "manufacturer", "notes": "Folding box for tea filters" } ] }'
```

**3. Bill of materials and SKU links**

Link components to units with `POST /api/v1/ppwr/links`, and articles (SKUs) to units with
`POST /api/v1/ppwr/article-links`, which reports an unknown article number as an error row.

```bash theme={null}
curl -X POST "https://app.polygon-one.com/api/v1/ppwr/links" \
  -H "Authorization: Bearer eudr_8f7d2a..." \
  -H "Content-Type: application/json" \
  -d '{ "rows": [ { "specInternalRef": "U-1", "componentCode": "K-1", "quantity": 1 } ] }'
```

**4. Recurring: push quantities per SKU, market and year**

```bash theme={null}
curl -X POST "https://app.polygon-one.com/api/v1/ppwr/quantities" \
  -H "Authorization: Bearer eudr_8f7d2a..." \
  -H "Content-Type: application/json" \
  -d '{ "rows": [ { "articleNumber": "SKU-1", "periodYear": 2026, "market": "DE", "quantity": 125000 } ], "replaceScope": "period-market" }'
```

`202` with a `jobId` — poll `GET /api/v1/import-jobs/{jobId}` as in the order sync. Each row's
`quantity` is the absolute total for that SKU, year and market and replaces the stored value —
never send increments, returns or credit notes as rows of their own; net them into the total.
Send `replaceScope: "period-market"` when the batch is the complete list for every year and
market it names; leave the field out when the batch carries only the SKUs that changed.

**5. Generate the report**

```bash theme={null}
curl -X POST "https://app.polygon-one.com/api/v1/ppwr/mengenmeldung/reports" \
  -H "Authorization: Bearer eudr_8f7d2a..." \
  -H "Content-Type: application/json" \
  -d '{ "periodYear": 2026, "market": "DE", "format": "lucid_de", "reportType": "HJM1", "systemOperatorId": "DE1234567890123" }'
```

A `422` lists item by item which data is not declarable yet — fix, re-push, generate again.

**6. Fetch the file and upload it to LUCID**

List the current report, then download it with `GET /api/v1/ppwr/mengenmeldung/{id}/xml`:

```bash theme={null}
curl "https://app.polygon-one.com/api/v1/ppwr/mengenmeldung/reports?periodYear=2026&market=DE&status=generated" \
  -H "Authorization: Bearer eudr_8f7d2a..."
```

Uploading the XML in LUCID is the one step that cannot be automated.

**What goes wrong most often**

* **Unit rows without `internalRef`.** Each repeat creates another unit.
* **Linking SKUs via the unit row's `articleNumbers`.** An unknown article number there is skipped;
  the response only counts such numbers (`articleNumbersUnmatched`) and does not name them.
  `POST /api/v1/ppwr/article-links` reports each one as an error row.
* **Overwriting a stored value without `overwrite: true`.** The whole batch is refused with
  `409 overwriteRequired`, nothing is written.
* **Expecting the feed to replace a hand-entered figure.** It never does; affected units come back in
  `conflicts`.

To write issued Declarations of Conformity back to your item master, see the ERP recipe under
[Reading Declarations of Conformity](https://help.polygon-one.com/en/api-reference/ppwr.md#reading-declarations-of-conformity).

## Before you go live

* **Key permissions per flow.** Order sync: `suppliers:write`,
  `articles:write`, `orders:write` ([Batch Import](https://help.polygon-one.com/en/api-reference/batch-import.md#permissions)); per-order
  origin additionally `plots:write` and `evidence:write`; plots: `plots:write`
  ([Geo analysis](https://help.polygon-one.com/en/api-reference/geo-analysis.md#permissions)); PPWR: `packaging:write`, plus
  `articles:write` if you create the SKUs over the API ([PPWR](https://help.polygon-one.com/en/api-reference/ppwr.md#permissions)).
  Write permissions include read. The **ERP integration** key preset includes all of these (a key
  created with it before 1 October 2026 lacks `plots:write` and `evidence:write`).
* **Modules and exports.** A key acts as a user. The PPWR endpoints need the PPWR module for that
  user. The plot report needs the EUDR module for that user and exports enabled for your workspace
  (otherwise `403`).
* **The rate limit is shared per IP.** 1000 requests per 60 seconds per client IP, across all `/api`
  paths and including people using the web app from the same network. Spread scheduled jobs and use
  the batch endpoints ([Rate limit](https://help.polygon-one.com/en/api-reference/errors-and-limits.md#rate-limit)).
* **Retry only what is safe to retry.** `429` with JSON and `5xx` with exponential backoff and
  jitter; batch imports are safe to replay. A repeated single-object supplier `POST` answers `409`
  (same name or `externalSupplierId`). A repeated single-object order `POST` answers `409`, and so
  does renaming an order onto an existing order number — look the order up first, or send orders as
  one-row batches ([Retries](https://help.polygon-one.com/en/api-reference/errors-and-limits.md#retries)).
* **There is no sandbox.** Every call acts on your live workspace
  ([Not in v1](https://help.polygon-one.com/en/api-reference/errors-and-limits.md#not-in-v1)).
* **Log `x-vercel-id`** on every failed call — support needs it
  ([Request reference](https://help.polygon-one.com/en/api-reference/errors-and-limits.md#request-reference-x-vercel-id)).
* **Ignore unknown response fields.** v1 adds fields over time
  ([Versioning](https://help.polygon-one.com/en/api-reference/introduction.md#versioning)).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.