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

# Upload GeoJSON Polygons for Deforestation Screening

> Submit GeoJSON farm polygons for satellite deforestation screening, poll the async job, and verify EUDR compliance status before linking them to a batch.

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.

<Note>
  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.
</Note>

## 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](https://datatracker.ietf.org/doc/html/rfc7946).
* A registered webhook endpoint to receive the `polygon.verified` completion event (see [Webhooks Setup](/guides/webhooks-setup)).

## GeoJSON format requirements

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

<Warning>
  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.
</Warning>

Here is a minimal valid feature:

```json theme={null}
{
  "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

<Steps>
  <Step title="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.

    <CodeGroup>
      ```python Python theme={null}
      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)
      ```

      ```javascript Node.js theme={null}
      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();
      ```

      ```bash cURL theme={null}
      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"
              }
            }
          ]
        }'
      ```
    </CodeGroup>

    A successful response looks like this:

    ```json theme={null}
    {
      "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`.
  </Step>

  <Step title="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`.

    <CodeGroup>
      ```python Python theme={null}
      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}")
      ```

      ```javascript Node.js theme={null}
      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();
      ```

      ```bash cURL theme={null}
      curl --request GET \
        --url https://live.agribackup.com/api/v1/enterprise/eudr/jobs/job_998877 \
        --header 'X-API-Key: sk_live_YOUR_API_KEY'
      ```
    </CodeGroup>
  </Step>

  <Step title="Receive the webhook completion event">
    Instead of polling, configure a webhook subscription for `polygon.verified` (see [Webhooks Setup](/guides/webhooks-setup)). AgriBackup posts the following payload to your endpoint when the job finishes:

    ```json theme={null}
    {
      "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.
  </Step>

  <Step title="Check individual polygon compliance status">
    Before linking a polygon to a batch, confirm its `eudrStatus` by calling `GET /api/v1/enterprise/eudr/polygons/{polygonId}`.

    <CodeGroup>
      ```python Python theme={null}
      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)
      ```

      ```javascript Node.js theme={null}
      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');
      ```

      ```bash cURL theme={null}
      curl --request GET \
        --url https://live.agribackup.com/api/v1/enterprise/eudr/polygons/poly_abc123 \
        --header 'X-API-Key: sk_live_YOUR_API_KEY'
      ```
    </CodeGroup>

    The response body uses the `PolygonStatusResponse` schema:

    ```json theme={null}
    {
      "polygonId": "poly_abc123",
      "commodity": "Coffee",
      "eudrStatus": "COMPLIANT",
      "areaHectares": 2.5,
      "createdAt": "2026-06-15T12:00:00Z"
    }
    ```
  </Step>
</Steps>

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

<Warning>
  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`.
</Warning>

<Tip>
  Use `GET /api/v1/enterprise/eudr/polygons?status=COMPLIANT` to list all verified compliant polygons for your organization, ready to reference in batch registration.
</Tip>
