Before you can register a batch or generate a Due Diligence Statement, you must upload the GeoJSON farm-boundary polygons that define where your commodity was grown. AgriBackup runs each polygon through Copernicus satellite imagery and assigns a deterministic EUDR compliance status. Because screening can take several seconds to minutes, the ingestion endpoint returns 202 Accepted immediately and processes the work asynchronously. You track progress with a job ID returned in the response body, and receive a polygon.verified webhook event when the job completes.
Ingestion accepts up to 5,000 features per request. If your supply chain exceeds this limit, split the payload into multiple requests and include a unique Idempotency-Key header for each one.
Prerequisites
- A live or sandbox API key (prefix
sk_live_ or sk_test_).
- Your farm boundaries exported as a GeoJSON
FeatureCollection conforming to RFC 7946.
- A registered webhook endpoint to receive the
polygon.verified completion event (see Webhooks Setup).
Each feature in your features array must satisfy the following:
type must be "Feature".
geometry.type must be "Polygon" or "MultiPolygon".
- Coordinates follow [longitude, latitude] order (RFC 7946 §3.1.1).
- The first and last coordinate of each ring must be identical (closed ring).
- Include a
properties object with at minimum farmer_id, commodity, area, and unit.
Polygons with self-intersecting rings or coordinate arrays fewer than 4 positions are rejected synchronously with a 400 Bad Request. Validate your GeoJSON client-side before sending.
Here is a minimal valid feature:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[36.8, -1.2],
[36.9, -1.2],
[36.9, -1.3],
[36.8, -1.3],
[36.8, -1.2]
]
]
},
"properties": {
"farmer_name": "Global Coffee Farmer #1",
"farmer_id": "TEST_FARMER_100",
"plot_name": "Nyeri Hill Farm Block B",
"area": 2.5,
"unit": "HECTARES",
"commodity": "Coffee"
}
}
]
}
Step-by-step ingestion
Submit the polygon ingestion request
Send a POST to /api/v1/enterprise/eudr/polygons with your GeoJSON features array. Include a UUID Idempotency-Key header so that retries on network failures do not create duplicate jobs.The endpoint responds with 202 Accepted and a JobAcceptedResponse body containing the jobId you will use to poll progress.import agribackup
from agribackup.rest import ApiException
import uuid
configuration = agribackup.Configuration()
configuration.api_key['ApiKeyAuth'] = 'sk_live_YOUR_API_KEY'
polygon_payload = {
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[36.8, -1.2],
[36.9, -1.2],
[36.9, -1.3],
[36.8, -1.3],
[36.8, -1.2]
]
]
},
"properties": {
"farmer_name": "Global Coffee Farmer #1",
"farmer_id": "TEST_FARMER_100",
"plot_name": "Nyeri Hill Farm Block B",
"area": 2.5,
"unit": "HECTARES",
"commodity": "Coffee"
}
}
]
}
with agribackup.ApiClient(configuration) as api_client:
try:
polygons_api = agribackup.EnterprisePolygonsApi(api_client)
job = polygons_api.ingest_polygons(
idempotency_key=str(uuid.uuid4()),
polygon_ingestion_request=polygon_payload
)
print("Job accepted:", job.job_id)
print("Estimated duration (s):", job.estimated_duration_sec)
except ApiException as e:
print("Error:", e)
import { AgriBackupClient } from '@agribackup/sdk';
import { randomUUID } from 'crypto';
const client = new AgriBackupClient({ apiKey: 'sk_live_YOUR_API_KEY' });
const polygonPayload = {
features: [
{
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [
[
[36.8, -1.2],
[36.9, -1.2],
[36.9, -1.3],
[36.8, -1.3],
[36.8, -1.2]
]
]
},
properties: {
farmer_name: 'Global Coffee Farmer #1',
farmer_id: 'TEST_FARMER_100',
plot_name: 'Nyeri Hill Farm Block B',
area: 2.5,
unit: 'HECTARES',
commodity: 'Coffee'
}
}
]
};
async function ingestPolygons() {
try {
const job = await client.polygons.ingest(polygonPayload, {
idempotencyKey: randomUUID()
});
console.log('Job accepted:', job.jobId);
console.log('Estimated duration (s):', job.estimatedDurationSec);
} catch (error) {
console.error('Error:', error.message);
}
}
ingestPolygons();
curl --request POST \
--url https://live.agribackup.com/api/v1/enterprise/eudr/polygons \
--header 'X-API-Key: sk_live_YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000' \
--data '{
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[36.8, -1.2],
[36.9, -1.2],
[36.9, -1.3],
[36.8, -1.3],
[36.8, -1.2]
]
]
},
"properties": {
"farmer_name": "Global Coffee Farmer #1",
"farmer_id": "TEST_FARMER_100",
"plot_name": "Nyeri Hill Farm Block B",
"area": 2.5,
"unit": "HECTARES",
"commodity": "Coffee"
}
}
]
}'
A successful response looks like this:{
"jobId": "job_998877",
"status": "PENDING",
"estimatedDurationSec": 3,
"createdAt": "2026-06-15T12:00:00Z"
}
The Location response header also contains the job-status URL: /api/v1/enterprise/eudr/jobs/job_998877. Poll the job status
Query GET /api/v1/enterprise/eudr/jobs/{jobId} to track processing phases. The job moves through the following phase values before finishing:| Phase | Meaning |
|---|
IMPORTING | Raw GeoJSON is being parsed and stored |
ANALYZING | Copernicus satellite imagery check in progress |
ANCHORING | Compliance results being anchored on Hedera |
GENERATING_CERTS | Cryptographic certificates being issued |
COMPLETED | All polygons processed; polygonIds are ready |
FAILED | One or more fatal errors; check errors[] |
Poll at a reasonable interval (for example, every 5 seconds) until phase is COMPLETED or FAILED.import agribackup
from agribackup.rest import ApiException
import time
configuration = agribackup.Configuration()
configuration.api_key['ApiKeyAuth'] = 'sk_live_YOUR_API_KEY'
with agribackup.ApiClient(configuration) as api_client:
jobs_api = agribackup.EnterpriseJobsApi(api_client)
job_id = "job_998877"
while True:
try:
status = jobs_api.get_job_status(job_id=job_id)
print(f"Phase: {status.phase} — processed {status.processed_records}/{status.total_records}")
if status.phase in ("COMPLETED", "FAILED"):
break
time.sleep(5)
except ApiException as e:
print("Error polling job:", e)
break
if status.phase == "COMPLETED":
print("Compliant polygons:", status.compliant_units)
print("High-risk polygons:", status.high_risk_units)
else:
for error in status.errors:
print(f"Error [{error.code}] on {error.field}: {error.reason}")
import { AgriBackupClient } from '@agribackup/sdk';
const client = new AgriBackupClient({ apiKey: 'sk_live_YOUR_API_KEY' });
async function pollJob(jobId) {
while (true) {
const status = await client.jobs.getStatus(jobId);
console.log(`Phase: ${status.phase} — processed ${status.processedRecords}/${status.totalRecords}`);
if (status.phase === 'COMPLETED' || status.phase === 'FAILED') {
return status;
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
async function main() {
const status = await pollJob('job_998877');
if (status.phase === 'COMPLETED') {
console.log('Compliant polygons:', status.compliantUnits);
console.log('High-risk polygons:', status.highRiskUnits);
} else {
status.errors.forEach(err =>
console.error(`Error [${err.code}] on ${err.field}: ${err.reason}`)
);
}
}
main();
curl --request GET \
--url https://live.agribackup.com/api/v1/enterprise/eudr/jobs/job_998877 \
--header 'X-API-Key: sk_live_YOUR_API_KEY'
Receive the webhook completion event
Instead of polling, configure a webhook subscription for polygon.verified (see Webhooks Setup). AgriBackup posts the following payload to your endpoint when the job finishes:{
"eventType": "polygon.verified",
"eventId": "evt_9988776655",
"timestamp": "2026-06-15T12:00:45Z",
"attempt": 0,
"data": {
"jobId": "job_998877",
"polygonsVerified": 150,
"polygonsFailed": 3,
"status": "COMPLETED",
"polygonIds": ["poly_abc123", "poly_def456"]
}
}
Store the polygonIds array — you will pass these IDs into the batch registration request. Check individual polygon compliance status
Before linking a polygon to a batch, confirm its eudrStatus by calling GET /api/v1/enterprise/eudr/polygons/{polygonId}.import agribackup
from agribackup.rest import ApiException
configuration = agribackup.Configuration()
configuration.api_key['ApiKeyAuth'] = 'sk_live_YOUR_API_KEY'
with agribackup.ApiClient(configuration) as api_client:
try:
polygons_api = agribackup.EnterprisePolygonsApi(api_client)
polygon = polygons_api.get_polygon_status(polygon_id="poly_abc123")
print("Status:", polygon.eudr_status)
print("Area (ha):", polygon.area_hectares)
except ApiException as e:
print("Error:", e)
import { AgriBackupClient } from '@agribackup/sdk';
const client = new AgriBackupClient({ apiKey: 'sk_live_YOUR_API_KEY' });
async function checkPolygon(polygonId) {
try {
const polygon = await client.polygons.getStatus(polygonId);
console.log('Status:', polygon.eudrStatus);
console.log('Area (ha):', polygon.areaHectares);
} catch (error) {
console.error('Error:', error.message);
}
}
checkPolygon('poly_abc123');
curl --request GET \
--url https://live.agribackup.com/api/v1/enterprise/eudr/polygons/poly_abc123 \
--header 'X-API-Key: sk_live_YOUR_API_KEY'
The response body uses the PolygonStatusResponse schema:{
"polygonId": "poly_abc123",
"commodity": "Coffee",
"eudrStatus": "COMPLIANT",
"areaHectares": 2.5,
"createdAt": "2026-06-15T12:00:00Z"
}
Polygon compliance statuses
eudrStatus | Meaning | Next step |
|---|
PENDING | Satellite analysis not yet complete | Wait for the polygon.verified webhook |
COMPLIANT | No deforestation signal detected | Include polygonId in a batch |
HIGH_RISK | Deforestation signal detected | Do not include in a batch; investigate with your supplier |
Only polygons with eudrStatus: COMPLIANT may be included in a batch registration. Attempting to include a PENDING or HIGH_RISK polygon returns a 400 Bad Request.
Use GET /api/v1/enterprise/eudr/polygons?status=COMPLIANT to list all verified compliant polygons for your organization, ready to reference in batch registration.