Skip to content
PlotDR
Get started

Getting started with the PlotDR API

The PlotDR API screens plot geometries against open satellite datasets and returns a verdict: compliant, non_compliant or needs_review. It is the same API the PlotDR dashboard uses, so anything you create through it also appears in your dashboard.

This guide takes you from an API key to your first verdict. Every endpoint, field and response is listed in the API reference.

Before you start

API access requires the API Pro plan (see Pricing). An organization admin can upgrade under Billing in the dashboard.

To create an API key:

  1. Sign in to the dashboard as an organization admin.
  2. Open Org Settings and go to the API keys section.
  3. Enter a key name, for example the name of the system that will use it, and select Create key.
  4. Copy the secret straight away. It is shown only once: PlotDR does not store the secret itself, so a lost key cannot be recovered. Create a new one instead.

Keys do not expire. Revoke a key from the same section as soon as you no longer need it; a revoked key stops working immediately. Keep keys on your servers, never in a browser, a mobile app or a public repository.

A key belongs to your organization, not to a person. If the organization leaves the API Pro plan, every key is refused with 403 until it returns to API Pro.

Base URL and authentication

All endpoints in this guide are under:

https://api.plotdr.ch/v1

Send your key as a bearer token in the Authorization header of every request. The examples below read it from an environment variable:

export PLOTDR_API_KEY="YOUR_API_KEY"

curl https://api.plotdr.ch/v1/orgs/me \
  -H "Authorization: Bearer $PLOTDR_API_KEY"

This returns your organization, with "plan": "api_pro". A missing or unknown key returns 401.

An API key can read and write your suppliers, plots, checks, batches and reports. Organization administration is reserved for a signed-in admin in the dashboard: updating the organization profile, inviting or removing team members, creating or revoking API keys, managing billing, and deleting the organization or permanently purging records. Those endpoints answer 403 to an API key.

Your first check

A check screens one geometry. You can run it on a registered plot, which keeps the plot and its check history together, or ad hoc on a geometry you do not want to store as a plot. This walkthrough registers a plot.

1. Find the commodity

Every plot and check names one of the seven EUDR commodities: cattle, cocoa, coffee, oil_palm, rubber, soy or wood. List them to get their IDs:

curl https://api.plotdr.ch/v1/commodities \
  -H "Authorization: Bearer $PLOTDR_API_KEY"

2. Create a supplier

A plot belongs to a supplier. country is a two-letter code in capitals, which should be an ISO 3166-1 alpha-2 code; PlotDR checks only the format:

curl https://api.plotdr.ch/v1/suppliers \
  -H "Authorization: Bearer $PLOTDR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Example Cooperative", "country": "CI"}'

The response is 201 Created with the supplier’s id.

3. Register the plot

Send the geometry as a GeoJSON Polygon, MultiPolygon or Point, with coordinates in WGS84 longitude and latitude order. EUDR Article 2(28) asks for coordinates with at least six decimal digits. PlotDR does not enforce this, so check your data before you submit it. This polygon is a plot of about 3 hectares:

curl https://api.plotdr.ch/v1/plots \
  -H "Authorization: Bearer $PLOTDR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "supplier_id": "SUPPLIER_ID",
    "commodity_id": "COMMODITY_ID",
    "external_ref": "FARM-0042",
    "geometry": {
      "type": "Polygon",
      "coordinates": [[
        [-6.601032, 5.780014],
        [-6.599218, 5.780127],
        [-6.599105, 5.781408],
        [-6.600847, 5.781523],
        [-6.601032, 5.780014]
      ]]
    }
  }'

The response is 201 Created with the plot’s id and the area_ha PlotDR computed from the polygon. external_ref is your own reference; you can later search for it with GET /plots?q=FARM-0042.

Geometry rules:

  • A polygon must be valid, for example not self-intersecting, and the parts of a MultiPolygon must not overlap.
  • A geometry has no size limit, but one past a screening budget is stored and not screened: more than 100,000 coordinate positions, more than 100,000 hectares, or a bounding box of more than 25 million 10 m pixels (or more than 50 km on its longer side, however thin). Its check is needs_review with summary_code geometry_over_budget, no layers, and the budget in skip_reason (coordinate_budget, area_budget or pixel_budget). A skipped check still counts against your check quota.
  • A Point needs asserted_area_ha, the plot’s area in hectares. A plot over 4 hectares must be sent as a polygon. EUDR Article 2(28) sets this rule for every commodity except cattle; PlotDR applies it to every commodity, cattle included.

A geometry that breaks these rules is refused with a validation error naming the geometry field.

4. Run the check

curl -X POST https://api.plotdr.ch/v1/plots/PLOT_ID/checks \
  -H "Authorization: Bearer $PLOTDR_API_KEY"

The check runs while the request is open, so the response can take a few seconds. It returns 201 Created with the complete result. Abridged here: some fields are left out, and layers is cut to one entry:

{
  "id": "5a0f3c6e-8d2b-4f47-9a1e-2c7b9e4d1f08",
  "plot_id": "PLOT_ID",
  "commodity_id": "COMMODITY_ID",
  "risk_status": "compliant",
  "summary": "Compliant: no verdict-affecting forest-loss or alert signals found; the informational layers below may still show signals.",
  "summary_code": "compliant_no_wdpa",
  "eudr_cutoff_date": "2020-12-31",
  "eudr_scope_disclaimer": "This screening covers geospatial deforestation and fire-alert signals only. It is one input to, not a substitute for, the full risk assessment required under EUDR Article 10(2).",
  "layers": [
    {
      "layer_name": "hansen_lossyear",
      "as_of_version": "v1.13",
      "contribution": "none",
      "affects_verdict": true,
      "gates_verdict": true,
      "is_stale": false
    }
  ],
  "attributions": ["..."],
  "created_by": null,
  "created_at": "2026-09-27T09:14:03.512345+00:00"
}

To screen a geometry without registering a plot, send it to POST /checks with a commodity_id instead; the result has "plot_id": null.

An archived plot is never screened. Archiving a plot with DELETE /plots/{plot_id}, or its supplier with DELETE /suppliers/{supplier_id}, retires it: a check on it is refused with 409 Record archived, which creates no check and uses no quota. Its past checks stay readable.

5. Read the result

  • risk_status is the verdict: compliant, non_compliant or needs_review.
  • summary_code says which rule produced it, as a stable value your code can branch on. summary is the same information as an English sentence, for people; do not parse it. In the example, compliant_no_wdpa is a compliant verdict for which protected areas were not screened: PlotDR does not screen protected areas (WDPA), so every compliant check without a coverage gap (see compliant_partial_coverage below) carries this code.
  • layers lists every dataset queried, with its contribution (none, alert, overlap or not_covered) and the evidence in details. is_stale marks a layer past its expected update cadence. A row whose as_of_version is null could not be resolved: its query failed, or PlotDR holds no copy of the layer yet. Its contribution then reads none, but it means unavailable, not “nothing found”. The dashboard and PDF reports show it as Unavailable, CSV reports write unavailable in the contribution’s place, and the JSON report format adds a boolean unavailable to every layer row (true for such a row). GET /checks/{id} itself does not carry that field, so in API responses test as_of_version for null.
  • affects_verdict is true for a layer whose result can change risk_status, and false for a layer shown for context only.
  • gates_verdict is true for a layer the verdict depends on: if that layer could not be resolved for the plot, or is stale, the check is needs_review whatever the layer shows. It is false only for a layer that cannot affect the verdict in any way, today OPERA DIST-ALERT (opera_dist_alert_veg_dist_status) and OPERA DIST-ANN (opera_dist_ann_veg_dist_status); their is_stale is shown for transparency only. A context-only layer can still gate the verdict: primary forest, for example, has affects_verdict: false but gates_verdict: true.
  • The DIST-ANN row’s details.disturbances lists the confirmed disturbance classes and first-detection dates found in NASA’s yearly summaries. There is no annual summary for 2021 or 2022: a disturbance first detected in 2022 and confirmed in 2023 can appear in the 2023 summary with its 2022 date, which is not 2022 coverage. A year appears only once NASA publishes its annual summary and PlotDR’s yearly update has copied it. If any year’s status tile cannot be matched to its own date tile, the row lists the classes without any year’s dates.
  • attributions holds the notices the datasets’ licences require when you pass the result on.

Fetch a check again at any time with GET /checks/{check_id}, or list a plot’s checks, newest first, with GET /plots/{plot_id}/checks.

What a verdict means

PlotDR screens the geospatial part of EUDR’s deforestation-free requirement only. Every check response carries this statement in eudr_scope_disclaimer, and so does every check in a JSON or CSV report. PDF reports, and the PDF dossier in a dds bundle, print it in the report’s language:

This screening covers geospatial deforestation and fire-alert signals only. It is one input to, not a substitute for, the full risk assessment required under EUDR Article 10(2).

It is not a legality or forest-degradation check, and it does not assess the non-geospatial criteria of an EUDR risk assessment. A verdict is not legal advice.

Protected areas are not screened. WDPA protected-area data is excluded, so no verdict says anything about whether a plot overlaps a protected area.

  • compliant: none of the layers that can affect the verdict (affects_verdict: true) showed tree-cover loss in 2021 or later on land with at least 10% canopy cover in 2000, SBTN natural-forest loss (sbtn_forest_loss), a deforestation alert or a fire alert; every layer the verdict depends on (gates_verdict: true) was resolved and current, the Hansen land/water mask showed mapped land, and Hansen tree-cover loss coverage was confirmed for the plot. Loss on land below that canopy threshold does not affect the verdict, but can still appear in the hansen_lossyear row’s details. Context-only layers, such as OPERA DIST-ALERT and DIST-ANN, primary forest, peat, tree-cover gain, tree canopy in 2000 (its own row; its values feed the Hansen loss rule) or SBTN binary natural lands, can still show an overlap. compliant_partial_coverage means one or more layers other than Hansen loss and OPERA DIST-ALERT or DIST-ANN do not cover this plot (contribution: not_covered); every compliant check without such a coverage gap carries compliant_no_wdpa, as protected areas are not screened (see above). A compliant verdict is a screening result: it does not certify that a plot or product is deforestation-free or compliant with EUDR (see the Terms of Service).
  • non_compliant: Hansen Global Forest Change shows tree-cover loss in 2021 or later on land with at least 10% canopy cover in 2000. This is the only signal that produces an automated non_compliant. It is a screening result, not a legal finding.
  • needs_review: a person should look at this plot before you rely on the result. PlotDR returns it when a signal is uncertain or depends on context, such as a deforestation or fire alert or SBTN natural-forest loss (sbtn_forest_loss), and when the data behind a confident verdict is missing or out of date: a layer could not be resolved for the location, a layer is past its expected freshness, or forest-loss coverage could not be confirmed. It is also what a geometry over a screening budget gets (geometry_over_budget, with skip_reason set and no layers): it was not screened at all. It is not an automated fail, and not a pass; the summary_code and layers show what needs a look.

The Terms of Service list every dataset PlotDR screens against and what each one can do to a verdict.

Pagination

List endpoints return an object with a data array holding one page of results and a next_cursor. Pass limit (1 to 200, default 50) and, for the next page, cursor set to the previous response’s next_cursor; treat it as opaque. next_cursor is null on the last page. Here NEXT_CURSOR stands for the previous response’s next_cursor:

curl "https://api.plotdr.ch/v1/plots?limit=100&cursor=NEXT_CURSOR" \
  -H "Authorization: Bearer $PLOTDR_API_KEY"

Batches

To screen many plots, submit a batch rather than one request per plot. Send either plot_ids of registered plots or geometries for ad hoc checks, not both, with up to 500 items:

curl https://api.plotdr.ch/v1/batches \
  -H "Authorization: Bearer $PLOTDR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"plot_ids": ["PLOT_ID_1", "PLOT_ID_2", "PLOT_ID_3"]}'

Each geometries item is an object with geometry, commodity_id and, for a point, asserted_area_ha. Every item is checked when you submit the batch: one invalid geometry or unknown commodity_id in geometries refuses the whole batch with a validation error whose field names the item, such as geometries.3.geometry, and nothing is queued.

The response is 202 Accepted with the batch id and "status": "pending". Batches run in the background: poll GET /batches/{batch_id} every few seconds until status is complete or failed.

  • total and completed show progress.
  • check_ids lists the checks created. Fetch each one with GET /checks/{check_id}.
  • item_errors lists the plot_ids entries that were skipped, by index in your request, with the plot_id and a reason: plot_not_found when the ID does not match a plot in your organization, and plot_archived when it matches one of your plots that has been archived, as an archived plot is never screened. The schema also lists invalid_geometry and unknown_commodity, but a batch submitted through the API refuses those up front instead, as described above. A batch can be complete with some items skipped; if every item was skipped it has no checks, and a report over it is refused.
  • error is set on a failed batch. It is a generic message rather than the cause, may include a reference to quote to support, and can be null.

Reports

A report packages the results of one check or one completed batch as a file. Request one with exactly one of check_id or batch_id, and a format:

format What you get
pdf A PDF report of the check or batch.
csv The results as CSV.
json The results as JSON.
dds A ZIP bundle to help you file a Due Diligence Statement in the EU TRACES system: a GeoJSON of the batch’s plots, a PDF dossier and a README. Batches only.
curl https://api.plotdr.ch/v1/reports \
  -H "Authorization: Bearer $PLOTDR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"batch_id": "BATCH_ID", "format": "pdf"}'

The response is 202 Accepted with "status": "pending". Poll GET /reports/{report_id} while status is pending or running. Stop when it is complete or failed: download from download_url only when it is complete; if it is failed, request the report again. error on a failed report is a generic message rather than the cause; it may include a reference to quote to support, and can be null. The download link is pre-signed, so it needs no API key, and it expires one hour after the report completes (expires_at); request a new report if it has expired. A report over a batch can only be requested once the batch is complete: before that, and for a batch with no checks, the request is refused with a validation error naming batch_id.

PlotDR does not file the Due Diligence Statement for you. The dds bundle’s README lists the fields you complete yourself.

Errors

Every error response from the versioned customer API under /v1 has the content type application/problem+json (RFC 7807):

{
  "type": "https://plotdr.ch/problems/validation-error",
  "title": "Validation error",
  "status": 422,
  "detail": "1 field(s) failed validation",
  "instance": "/v1/plots",
  "errors": [{ "field": "commodity_id", "message": "Field required" }]
}
  • type identifies the kind of problem. It is either a URL under https://plotdr.ch/problems/, documented on the problem types pages, or about:blank, in which case the HTTP status and detail say what went wrong.
  • status repeats the HTTP status code.
  • title is a short English name that is the same for every occurrence of a type: the type’s name, such as Validation error, or for about:blank the HTTP status phrase, such as Not Found. detail explains this occurrence. Both are text for people and may change: branch on type and status, not on text.
  • instance is the path of the request.
  • errors appears on validation errors only, with one {field, message} per problem. A 422 caused by a value in your request is a validation error, whether the request did not match the schema or the API rejected a value such as a geometry, an unknown ID or a cursor, apart from a few dashboard-only cases listed on the validation error page that an API key never receives. field is a dotted path into your request, such as geometries.0.commodity_id, or null when the problem is the request as a whole. When the body is not valid JSON, field is instead the character offset of the parse error, such as "13", and message is JSON decode error.

Every response, including errors, carries an X-Request-ID header. Include it when you contact support about a failed request.

The unversioned operational readiness probe, GET /health/ready, is the exception to this error contract. When a dependency is unavailable it returns 503 with an application/json body that contains status and a checks map, so an uptime monitor can identify the failing dependency.

Rate limits

Creating checks, batches and reports is rate-limited per organization, in fixed one-minute windows. The budget is shared by all of your organization’s API keys and its dashboard users. For API Pro, the current limits are:

Endpoint Limit per minute
POST /checks and POST /plots/{id}/checks 300 checks, shared between the two endpoints
POST /batches 5,000 batch items, counted across all batches
POST /reports 60 reports

Successful requests to these endpoints, and requests rejected by their per-minute rate limit, carry these headers for that endpoint’s budget:

  • X-RateLimit-Limit: the budget for the current window.
  • X-RateLimit-Remaining: the units left in that budget. A batch uses one unit per item.
  • X-RateLimit-Reset: when the window resets, as a Unix timestamp in seconds.

When a per-minute rate limit rejects a request, the API answers 429 Too Many Requests with "type": "about:blank" and a Retry-After header giving the seconds to wait. A request that fails for another reason after the limit check may omit the rate-limit headers. If rate limiting is temporarily unavailable, requests are allowed through and the advertised remaining budget may not reflect actual use. Read the headers when available rather than hard-coding the numbers above, which may change.

Other limits apply to every request: a request body is at most 10 MB and a batch at most 500 items.

Next steps

  • The API reference documents every endpoint and schema, and offers the OpenAPI specification for download, for example to generate a client.
  • The problem types explain each error type and what to do about it.