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:
- Sign in to the dashboard as an organization admin.
- Open Org Settings and go to the API keys section.
- Enter a key name, for example the name of the system that will use it, and select Create key.
- 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
MultiPolygonmust 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_reviewwithsummary_codegeometry_over_budget, nolayers, and the budget inskip_reason(coordinate_budget,area_budgetorpixel_budget). A skipped check still counts against your check quota. - A
Pointneedsasserted_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_statusis the verdict:compliant,non_compliantorneeds_review.summary_codesays which rule produced it, as a stable value your code can branch on.summaryis the same information as an English sentence, for people; do not parse it. In the example,compliant_no_wdpais acompliantverdict for which protected areas were not screened: PlotDR does not screen protected areas (WDPA), so everycompliantcheck without a coverage gap (seecompliant_partial_coveragebelow) carries this code.layerslists every dataset queried, with itscontribution(none,alert,overlapornot_covered) and the evidence indetails.is_stalemarks a layer past its expected update cadence. A row whoseas_of_versionisnullcould not be resolved: its query failed, or PlotDR holds no copy of the layer yet. Itscontributionthen readsnone, but it means unavailable, not “nothing found”. The dashboard and PDF reports show it as Unavailable, CSV reports writeunavailablein the contribution’s place, and the JSON report format adds a booleanunavailableto every layer row (truefor such a row).GET /checks/{id}itself does not carry that field, so in API responses testas_of_versionfornull.affects_verdictistruefor a layer whose result can changerisk_status, andfalsefor a layer shown for context only.gates_verdictistruefor a layer the verdict depends on: if that layer could not be resolved for the plot, or is stale, the check isneeds_reviewwhatever the layer shows. It isfalseonly 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); theiris_staleis shown for transparency only. A context-only layer can still gate the verdict: primary forest, for example, hasaffects_verdict: falsebutgates_verdict: true.- The DIST-ANN row’s
details.disturbanceslists 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. attributionsholds 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 thehansen_lossyearrow’sdetails. 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_coveragemeans one or more layers other than Hansen loss and OPERA DIST-ALERT or DIST-ANN do not cover this plot (contribution: not_covered); everycompliantcheck without such a coverage gap carriescompliant_no_wdpa, as protected areas are not screened (see above). Acompliantverdict 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 automatednon_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, withskip_reasonset and nolayers): it was not screened at all. It is not an automated fail, and not a pass; thesummary_codeandlayersshow 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.
totalandcompletedshow progress.check_idslists the checks created. Fetch each one withGET /checks/{check_id}.item_errorslists theplot_idsentries that were skipped, byindexin your request, with theplot_idand areason:plot_not_foundwhen the ID does not match a plot in your organization, andplot_archivedwhen it matches one of your plots that has been archived, as an archived plot is never screened. The schema also listsinvalid_geometryandunknown_commodity, but a batch submitted through the API refuses those up front instead, as described above. A batch can becompletewith some items skipped; if every item was skipped it has no checks, and a report over it is refused.erroris set on afailedbatch. It is a generic message rather than the cause, may include a reference to quote to support, and can benull.
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" }]
}
typeidentifies the kind of problem. It is either a URL underhttps://plotdr.ch/problems/, documented on the problem types pages, orabout:blank, in which case the HTTP status anddetailsay what went wrong.statusrepeats the HTTP status code.titleis a short English name that is the same for every occurrence of atype: the type’s name, such asValidation error, or forabout:blankthe HTTP status phrase, such asNot Found.detailexplains this occurrence. Both are text for people and may change: branch ontypeandstatus, not on text.instanceis the path of the request.errorsappears on validation errors only, with one{field, message}per problem. A422caused 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 acursor, apart from a few dashboard-only cases listed on the validation error page that an API key never receives.fieldis a dotted path into your request, such asgeometries.0.commodity_id, ornullwhen the problem is the request as a whole. When the body is not valid JSON,fieldis instead the character offset of the parse error, such as"13", andmessageisJSON 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
typeand what to do about it.