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

# Async Architecture: Jobs, Webhooks, and Idempotency

> Learn why AgriBackup uses 202 Accepted responses, how to track jobs and consume webhooks, and when to use Idempotency-Key headers.

Several AgriBackup endpoints return `202 Accepted` rather than an immediate result. Understanding why — and how to work with the asynchronous job and webhook system — is essential before you build a production integration.

## Why 202 Accepted?

Two operations at the heart of the compliance pipeline cannot complete in a single HTTP round-trip:

**Satellite deforestation screening.** When you submit GeoJSON polygons, AgriBackup dispatches each feature to the Copernicus Data Space Ecosystem (CDSE) for analysis against Sentinel-2 imagery. Depending on imagery availability and the number of features, screening can take seconds to minutes. Holding the HTTP connection open for that duration is impractical for enterprise batch volumes.

**Hedera DLT anchoring.** Anchoring a batch, shipment link, or DDS to the Hedera Hashgraph requires waiting for network consensus. While Hedera's finality is fast (typically 3–5 seconds), the API cannot return a `200 OK` before the transaction ID is confirmed on-chain — doing so would allow you to build compliance logic on a transaction that might never reach consensus.

The endpoints that trigger one or both of these operations return `202 Accepted` immediately, with a job ID you use to track progress:

| Endpoint                                        | Async reason                                        |
| ----------------------------------------------- | --------------------------------------------------- |
| `POST /api/v1/enterprise/eudr/polygons`         | Satellite screening + Hedera anchoring              |
| `POST /api/v1/enterprise/eudr/batches`          | Hedera anchoring + Copernicus risk assessment       |
| `POST /api/v1/enterprise/eudr/shipments/link`   | Hedera smart contract execution                     |
| `POST /api/v1/enterprise/eudr/declarations/dds` | Hedera land tenure anchoring + TRACES NT submission |
| `POST /api/v1/enterprise/eudr/reports/archive`  | Bulk ZIP compilation                                |

## The 202 response payload

Every async endpoint returns a `JobAcceptedResponse` body and a `Location` header:

```json theme={null}
{
  "jobId": "job_998877",
  "status": "PENDING",
  "estimatedDurationSec": 3,
  "createdAt": "2026-06-15T12:00:00Z"
}
```

<ResponseField name="jobId" type="string" required>
  The unique ID you use to poll job status. Example: `job_998877`
</ResponseField>

<ResponseField name="status" type="string" required>
  Always `PENDING` at acceptance time.
</ResponseField>

<ResponseField name="estimatedDurationSec" type="integer" required>
  Advisory estimate of how long before the job completes or reaches the next phase.
</ResponseField>

<ResponseField name="createdAt" type="string" required>
  ISO-8601 timestamp of when the job was accepted.
</ResponseField>

The `Location` response header points directly to the job status URL, for example:

```http theme={null}
Location: /api/v1/enterprise/eudr/jobs/job_998877
```

## Polling the job endpoint

If you prefer polling over webhooks, call `GET /api/v1/enterprise/eudr/jobs/{jobId}` repeatedly. The response includes a `Retry-After` header when the job is still in progress, telling you how many seconds to wait before the next poll:

```json theme={null}
{
  "jobId": "job_998877",
  "organizationId": "org_test_tier1_alpha",
  "phase": "ANCHORING",
  "totalRecords": 10,
  "processedRecords": 7,
  "failedRecords": 0,
  "compliantUnits": 7,
  "highRiskUnits": 0,
  "merkleRootHash": "0xabc123...",
  "hederaTransactionId": "0.0.8713513@1713583200-000000000",
  "errors": [],
  "startedAt": 1713583200000,
  "completedAt": null
}
```

### Job phases

Jobs move through the following phases in order:

| Phase              | Description                                           |
| ------------------ | ----------------------------------------------------- |
| `IMPORTING`        | Records are being ingested and validated              |
| `ANALYZING`        | Copernicus satellite screening is running             |
| `ANCHORING`        | Hedera DLT anchoring is in progress                   |
| `GENERATING_CERTS` | DDS XML or archive ZIP is being compiled              |
| `COMPLETED`        | Job finished successfully                             |
| `FAILED`           | Job terminated with errors — check the `errors` array |

<Tip>
  Respect the `Retry-After` header. Polling more aggressively than its value recommends counts against your rate limit quota without providing useful information.
</Tip>

## Webhooks

Webhooks are the recommended way to react to async completion events in production. Rather than polling, you register an HTTPS endpoint and AgriBackup delivers a POST payload to it the moment each event fires.

### Registering a webhook

Call `POST /api/v1/enterprise/eudr/webhooks` with the URL you want to receive events and the event types you want to subscribe to:

```json theme={null}
{
  "targetUrl": "https://erp.example.com/agribackup-webhooks",
  "eventTypes": [
    "shipment.linked",
    "polygon.verified",
    "batch.risk_assessed",
    "dds.submitted",
    "dds.validated",
    "dds.rejected",
    "job.failed",
    "report.ready"
  ]
}
```

AgriBackup returns a `signingSecret` at registration time. Store this secret securely — you use it to verify every incoming payload.

### Webhook event types

| Event                 | Fired when                                                                  |
| --------------------- | --------------------------------------------------------------------------- |
| `polygon.verified`    | Satellite screening completes and the polygon receives a compliance verdict |
| `batch.risk_assessed` | A batch finishes Hedera anchoring and risk assessment                       |
| `shipment.linked`     | The Hedera smart contract confirms a batch-to-shipment link                 |
| `dds.submitted`       | A DDS has been sent to TRACES NT                                            |
| `dds.validated`       | TRACES NT confirms the DDS is valid                                         |
| `dds.rejected`        | TRACES NT rejects the DDS — the payload body contains the rejection reason  |
| `job.failed`          | Any async job terminates in the `FAILED` phase                              |
| `report.ready`        | A bulk archival ZIP is ready for download                                   |

### Verifying webhook payloads

AgriBackup signs every outbound webhook with HMAC-SHA256 using the signing secret you received at registration. The signature is sent in the `X-AgriBackup-Signature` header as a hex-encoded digest of the raw request body. Verify it before processing the payload:

<CodeGroup>
  ```python Python theme={null}
  import hmac
  import hashlib

  def verify_webhook(raw_body: bytes, signature_header: str, signing_secret: str) -> bool:
      expected = hmac.new(
          signing_secret.encode('utf-8'),
          raw_body,
          hashlib.sha256
      ).hexdigest()
      # Use constant-time comparison to prevent timing attacks
      return hmac.compare_digest(expected, signature_header)
  ```

  ```javascript Node.js theme={null}
  import crypto from 'crypto';

  function verifyWebhook(rawBody, signatureHeader, signingSecret) {
    const expected = crypto
      .createHmac('sha256', signingSecret)
      .update(rawBody)
      .digest('hex');
    // Use timingSafeEqual to prevent timing attacks
    return crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(signatureHeader)
    );
  }
  ```

  ```java Java theme={null}
  import javax.crypto.Mac;
  import javax.crypto.spec.SecretKeySpec;
  import java.security.MessageDigest;

  public boolean verifyWebhook(byte[] rawBody, String signatureHeader, String signingSecret)
      throws Exception {
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(signingSecret.getBytes("UTF-8"), "HmacSHA256"));
    String expected = bytesToHex(mac.doFinal(rawBody));
    // Constant-time comparison
    return MessageDigest.isEqual(expected.getBytes(), signatureHeader.getBytes());
  }
  ```
</CodeGroup>

<Warning>
  Always verify the `X-AgriBackup-Signature` header before acting on a webhook payload. Processing unsigned or tampered payloads could cause you to act on fraudulent compliance events.
</Warning>

### Dead-letter queue

If AgriBackup cannot deliver a webhook after repeated retries (for example, your endpoint is down), the event moves to the dead-letter queue (DLQ). Retrieve permanently failed events with `GET /api/v1/enterprise/eudr/webhooks/dlq` and replay individual or bulk events back to your primary endpoint with the `replay` endpoints.

## Async flow sequence

The following diagram illustrates the full asynchronous flow for polygon ingestion:

```text theme={null}
Your ERP                    AgriBackup API           Copernicus CDSE      Hedera Hashgraph
   │                              │                        │                    │
   │─── POST /polygons ──────────►│                        │                    │
   │◄── 202 Accepted (jobId) ─────│                        │                    │
   │                              │─── Screen polygons ───►│                    │
   │─── GET /jobs/{jobId} ───────►│  (phase: ANALYZING)    │                    │
   │◄── 200 phase: ANALYZING ─────│                        │                    │
   │                              │◄── Verdicts ───────────│                    │
   │                              │─── Anchor on Hedera ───────────────────────►│
   │                              │◄── Consensus tx ID ────────────────────────│
   │                              │  (phase: COMPLETED)    │                    │
   │◄══ POST polygon.verified ════│                        │                    │
   │    (webhook delivery)        │                        │                    │
```

1. You POST the polygon payload and immediately receive `202 Accepted` with a `jobId`.
2. AgriBackup queues satellite screening with Copernicus CDSE.
3. You can optionally poll `GET /jobs/{jobId}` — the `Retry-After` header guides your polling interval.
4. Once screening completes, AgriBackup anchors the results on Hedera and waits for consensus.
5. On consensus, the job moves to `COMPLETED` and AgriBackup delivers the `polygon.verified` webhook to your registered endpoint.

## Idempotency keys

Mutating endpoints that are either asynchronous or expensive to retry accept an `Idempotency-Key` header. Pass a unique string per logical operation — a UUID, a reference from your ERP, or any value that uniquely identifies the request:

```bash theme={null}
curl -X POST https://live.agribackup.com/api/v1/enterprise/eudr/polygons \
  -H "X-API-Key: sk_live_YOUR_API_KEY" \
  -H "Idempotency-Key: tx_erp_ref_20260715_001" \
  -H "Content-Type: application/json" \
  -d '{"features": [...]}'
```

If your network times out before you receive the `202 Accepted`, resend the exact same request with the same `Idempotency-Key`. AgriBackup returns the original `202` response instead of creating a duplicate job. This is safe to retry any number of times.

<Note>
  Idempotency keys are scoped to your organization and expire after 24 hours. Use a key that encodes enough context (for example, `batch_BR-2026-991_ingest`) to be meaningful in logs and support tickets.
</Note>

### When to always include an Idempotency-Key

* `POST /api/v1/enterprise/eudr/polygons` — polygon ingestion
* `POST /api/v1/enterprise/eudr/batches` — batch registration
* `POST /api/v1/enterprise/eudr/shipments/link` — shipment linkage
* `POST /api/v1/enterprise/eudr/declarations/dds` — DDS generation

Omitting the key on these endpoints means a network-level retry will create a second job.
