> ## 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.

# Geo analysis over the API

> Send a plot geometry, wait for the deforestation analysis, read the result as JSON and download the plot report as PDF

## Overview

You send a plot geometry, the deforestation analysis runs automatically, and you read the result as
JSON and download the plot report as PDF. No article or supplier is needed — a plot with a commodity
group is enough.

| Step | Endpoint |
| :- | :- |
| Optional: check the geometry | `POST /api/v1/plots/validate` |
| Create the plot | `POST /api/v1/plots` |
| Poll status and result | `GET /api/v1/plots/{id}?fields=analysisStatus,analysis` |
| Download the plot report | `GET /api/v1/plots/{id}/report` |

## Permissions

Key mechanism, base URL and the general status codes are in the
[Introduction](/en/api-reference/introduction); how to create a key is under
[Settings → API Keys](/en/eudr/einstellungen#api-keys). Plots exist in the EUDR module only.
`plots:write` covers everything; `plots:read` is enough to read the result and download the report.

<Info>
  In a customer workspace, the report returns `403` with `EUDR compliance module is not enabled`
  unless the EUDR module is enabled for the user the key acts as (a personal key: its owner; a company
  key: the workspace's highest-ranking active user). The result JSON has no such check.
</Info>

## Creating a plot

`POST /api/v1/plots` creates **one** plot and starts its analysis. All fields are in the
[endpoint reference](/en/api-reference/endpoint/plots/create); what an integration needs to know:

* `geojson` is required (see [Geometry](#geometry)).
* `commodityGroup` is required unless you send `supplierArticleIds`: one of `cattle`, `cocoa`,
  `coffee`, `oil_palm`, `rubber`, `soya`, `wood`. The API also accepts `non_relevant` (listed in the
  error message); do not send it for a plot you want analyzed. With `supplierArticleIds`, the group is
  then derived from the articles.
* For `wood`, `species` is required — each entry with `commonName` **and** `scientificName`.
* Put your own plot reference in `name` (see [Idempotency](#idempotency)).
* `country` (ISO 3166-1 alpha-2, e.g. `BR`): without it (or with a value that does not resolve to a
  country), the low-risk skip never applies and the plot is always analyzed.

### Geometry

* Accepted: `Polygon`, `MultiPolygon`, `Point` and `MultiPoint` — as a bare geometry or as a
  `Feature`. Coordinates in WGS84, order `[longitude, latitude]`; an altitude (third value) is
  accepted but not stored. Coordinates outside longitude ±180 / latitude ±90 are rejected; nothing
  is saved.
* **One request creates one plot.** A `FeatureCollection` is rejected — send each feature as its own
  request. Other types (`LineString`, `GeometryCollection`, …) are rejected too.
* When `size` is given, a `Point` or `MultiPoint` is only accepted up to 4 ha (20,000 ha when
  `commodityGroup` is `cattle`); above that the EUDR requires a polygon. Without `size` there is no
  area check.

### Quality checks

Before storing, the API runs the same geometry checks as the import in the app and stores the result:

| Check | What happens |
| :- | :- |
| Coordinates | Rounded to 6 decimal places; altitude dropped; duplicate consecutive points removed; open rings closed; ring orientation normalized |
| Holes (interior rings) | Removed — warning `POLYGON_HOLES_REMOVED` |
| Self-intersection | Repaired if the repair changes the area by at most 2 % (the result can be a `MultiPolygon`) — warning `POLYGON_SELF_INTERSECTION_REPAIRED`; a hole the repair encloses is then removed like any hole. Otherwise refused |
| Precision | Counted from the digits as written in the request (`-47.060100` counts as 6); the value with the fewest decimals decides (holes and altitude are not counted). 6 or more: OK; 5: warning `COORDINATE_PRECISION_BORDERLINE`; 4: warning `COORDINATE_PRECISION_LOW`; 3 or fewer: refused |

**Removing a hole — including one the self-intersection repair encloses — makes the excluded area
part of the plot: the plot gets larger.** The API never swaps latitude and longitude. The create
response lists the codes above in `warnings` (`[{ "code": … }]`); rounding, dropped altitude, removed
duplicate points and closed or reoriented rings are not listed. On a deduplicated answer `warnings` is
always empty, because that request stored nothing.

### Idempotency

If you send the same plot again — same `name`, same `country`, same geometry (compared after the quality checks) and same
`supplierArticleIds` — the API creates **no** second plot and starts **no** second analysis; it answers
with the stored plot and `"deduplicated": true`. Name and country are compared ignoring case, leading
and trailing spaces, and repeated spaces. Other fields of a repeat, such as `commodityGroup` or `size`,
are not applied. The response carries `"updated": true` when producer, production dates or species of
the repeat were applied, otherwise `false`; the field is absent when a new plot was created. A repeat of a plot stored before these checks
existed is recognised too: the geometry exactly as sent is compared as well.

## Checking a geometry without creating a plot

`POST /api/v1/plots/validate` takes the same body as create and runs the same checks, but writes
nothing — no plot, no analysis (a dry run). `plots:read` is enough, and `commodityGroup` is optional.
A request that create would refuse with a `code` gets `200` with `valid: false` and that code; a body
that is not JSON is a `400`. `valid` says whether create would accept the request, apart from the
points listed under "Not checked" below. For a polygon with one hole, written with 6 decimals:

```json theme={null}
{
  "valid": true,
  "errors": [],
  "warnings": [{ "code": "POLYGON_HOLES_REMOVED" }],
  "precision": { "digits": 6, "class": "ok" },
  "geometry": { "type": "Polygon", "coordinates": [[[-47.060123, -21.181934], [-47.058531, -21.181934], [-47.058531, -21.180245], [-47.060123, -21.180245], [-47.060123, -21.181934]]] },
  "measuredAreaHa": 3.1
}
```

`geometry` is the geometry create would store, `measuredAreaHa` its area — 3.1 ha instead of the
2.68 ha of the polygon with its hole; `null` for points. With `valid: false`, `errors` holds the `code` and `message` create would answer,
`geometry` and `measuredAreaHa` are `null`, and `precision` is `null` if the request was refused before
the precision was measured. The dry run gives the point area cap and an invalid `size` a code
(`plot_point_area_too_large`, `plot_size_invalid`); create answers these without one. Not checked:
whether `commodityGroup` is missing without `supplierArticleIds` (create answers
`plot_commodity_group_required`), whether the `supplierArticleIds` belong to your company, and whether
the same plot already exists.

## Polling the status

| `analysisStatus` | Meaning |
| :- | :- |
| `pending` | Created, or the geometry was changed; analysis not started yet |
| `in_progress` | Analysis running |
| `completed` | Analysis finished; `riskLevel` is `LOW`, `MODERATE` or `HIGH` |
| `failed` | The analysis could not run, or the plot failed the plausibility check (`riskLevel: INVALID`), e.g. because it overlaps water or urban areas |
| `exempt` | The declared production end (`productionDateTo`) is on or before 31 December 2020; no analysis runs |
| `skipped` | The plot lies in an EUDR low-risk country and **Geo analysis for low-risk countries** is not enabled for your company (see [Settings](/en/eudr/einstellungen#analysis-for-low-risk-countries-art-13)) |

Poll every **15 to 30 seconds** while the status is `pending` or `in_progress`. The app applies no
per-key rate limit to these endpoints.

If the analysis is `failed` or `skipped`, you can have the plot analyzed again in the app with
**Re-analyze** (see [Manage Plots](/en/eudr/optional/plots)). After **Re-analyze**, the status keeps
its previous value until the analysis starts.

## Reading the result

`analysis` holds the **last stored** result. It is `null` when none was ever stored or after the
geometry changed. While a re-analysis is running (`in_progress`) or after a re-analysis failed, it can
still hold the previous result — compare `assessedAt`. The texts are **English** and identical to the
plot report.

```json theme={null}
{
  "analysisStatus": "completed",
  "analysis": {
    "riskLevel": "LOW",
    "assessedAt": "2026-09-29T09:14:03.000Z",
    "conclusion": "The recorded analysis did not establish post-cutoff deforestation or forest degradation for this plot.",
    "limitations": [],
    "evidence": [
      { "dataset": "JRC GFC 2020", "measurementStatus": "Recorded value; dataset plot coverage not stored", "observation": "Forest baseline value: 62.4% of plot", "relevance": "Forest baseline at cutoff" },
      { "dataset": "Hansen GFC", "measurementStatus": "Measured", "observation": "Post-2020 loss: 0.0% of JRC baseline forest", "relevance": "Tree-cover loss after cutoff" }
    ]
  }
}
```

New `riskLevel` values may be added; treat a value you do not know as needing review. The
`evidence[].dataset` names are stable (`JRC GFC 2020`, `Hansen GFC`, `GFW alerts`,
`JRC TMF AnnualChange`, and `JRC GFT 2020` for `wood` only); `measurementStatus`, `observation` and
`relevance` are human-readable and their wording may change — do not match on them.

### Risk levels

| `riskLevel` | `conclusion` |
| :- | :- |
| `LOW` | The recorded analysis did not establish post-cutoff deforestation or forest degradation for this plot. |
| `MODERATE` | The recorded analysis did not resolve whether post-cutoff deforestation or forest degradation occurred; verification remains needed. |
| `HIGH` | The recorded analysis identified evidence requiring this plot to be treated as high geospatial risk; this does not by itself determine EUDR non-compliance. |
| `INVALID` | The recorded analysis could not produce a valid geospatial assessment because the recorded plot or data-quality checks failed. |

<Warning>
  This is a geospatial risk assessment, not a standalone determination of EUDR compliance.
</Warning>

The legacy numeric field `deforestationRiskResult` is `0` low (also set for `exempt`), `1` high, `2`
moderate, or `null` for no result (`pending`, `skipped`, `failed` including `INVALID`); during a
re-analysis or after a failed re-analysis it can still hold the previous value. Base your evaluation on `analysisStatus` and `analysis`.

## Plot report (PDF)

`GET /api/v1/plots/{id}/report` returns the plot report as a PDF attachment. It is **English** and
carries the same evidence rows as `analysis.evidence`. The file name is `plot-report-<name>.pdf`, where
every character other than letters, digits, `_` and `-` becomes `_` (`plot-report-plot.pdf` without a
name).

<Warning>
  Every call renders the report anew. Download it **once**, when the result exists — poll the status
  with `GET /api/v1/plots/{id}`, not with the report.
</Warning>

## Error codes

Rejected requests create nothing. The body holds `error` and, in the cases below, a machine-readable
`code`:

```json theme={null}
{
  "error": "Plot geometry has too many parts: 312 (maximum 250)",
  "code": "plot_geometry_too_many_members",
  "limit": 250,
  "actual": 312
}
```

| `code` | Status | When |
| :- | :- | :- |
| `plot_geometry_missing` | `400` | `geojson` is missing or not an object |
| `plot_geometry_type_unsupported` | `400` | `FeatureCollection` or any other type than `Polygon`, `MultiPolygon`, `Point`, `MultiPoint`; `geometryType` names the type sent |
| `plot_commodity_group_invalid` | `400` | `commodityGroup` is not an allowed value; the message lists the allowed values |
| `plot_commodity_group_required` | `400` | Neither `commodityGroup` nor `supplierArticleIds` |
| `plot_geometry_too_many_members` | `400` | More than 250 parts (polygons of a `MultiPolygon`, points of a `MultiPoint`) |
| `plot_geometry_bbox_spread_too_wide` | `400` | Bounding box wider than 1.0° per axis; `actual: -1` means the geometry could not be measured |
| `plot_geometry_area_too_large` | `400` | Total area above 100,000 ha |
| `plot_coordinates_out_of_range` | `400` | A coordinate outside longitude ±180 / latitude ±90, or not a number |
| `plot_supplier_article_ids_invalid` | `400` | An entry of `supplierArticleIds` is not a UUID, or more than 5,000 ids |
| `plot_species_incomplete` | `400` | A species without common or scientific name, more than 500 species, or a name over 200 characters |
| `plot_species_required` | `400` | `wood` without a species with a scientific name |
| `plot_geometry_too_many_vertices` | `400` | More than 20,000 points, counted as sent |
| `plot_geometry_invalid` | `400` | The geometry fails the checks, e.g. a ring with fewer than three distinct points |
| `plot_geometry_self_intersection_unrepairable` | `400` | Self-intersection that cannot be repaired, or whose repair would change the area by more than 2 % |
| `plot_precision_too_low` | `400` | 3 or fewer decimal places; `precisionDigits` gives the count |

`plot_geometry_too_many_members`, `plot_geometry_bbox_spread_too_wide` and
`plot_geometry_area_too_large` carry `limit` and `actual` in the same unit (parts, degrees, hectares);
split into several plots. For `plot_geometry_too_many_vertices`, simplify the geometry. These answer
without a `code`: `400` when the body is not JSON; `400` when a `Point` or `MultiPoint` is above the area
cap for its `size` (see [Geometry](#geometry)); `400` when `size` is not a finite number ≥ 0; `403` when
a supplier article does not belong to your company.

## Recipe: from GeoJSON to report

<Steps>
  <Step title="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",
        "size": 3.1,
        "species": [{ "commonName": "Eucalyptus", "scientificName": "Eucalyptus grandis" }],
        "productionDateFrom": "2026-01-01",
        "productionDateTo": "2026-06-30",
        "geojson": {
          "type": "Polygon",
          "coordinates": [[[-47.060123, -21.180245], [-47.058531, -21.180245], [-47.058531, -21.181934],
                           [-47.060123, -21.181934], [-47.060123, -21.180245]]]
        }
      }'
    ```

    The response is `201` with the plot under `created` and `warnings` (empty here). Take
    `created.id`; on a repeat the response carries `"deduplicated": true`.
  </Step>

  <Step title="Poll until the analysis is done, then read the result">
    Every 15 to 30 seconds, until `analysisStatus` is no longer `pending` or `in_progress`; the result
    is then in `analysis`:

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

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