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

# Set Up Webhooks to Receive EUDR Compliance Events

> Register an HTTPS endpoint, verify HMAC-SHA256 signatures, handle EUDR compliance events, rotate webhook secrets, and manage failed deliveries in the DLQ.

AgriBackup's asynchronous architecture relies on webhooks to notify your system when long-running compliance jobs complete. Rather than polling every endpoint on a schedule, you register a single HTTPS callback URL and subscribe to the event types you care about. Each delivery is signed with HMAC-SHA256 so you can cryptographically verify that the payload arrived from AgriBackup and has not been tampered with. This guide walks you through registration, signature verification, event handling, secret rotation, and dead-letter queue (DLQ) management.

<Warning>
  Your webhook endpoint must respond with a `2xx` status code **within 10 seconds** of receiving a delivery. If it times out or returns a non-2xx status, AgriBackup retries delivery with exponential back-off. After 5 failed attempts, the event is moved to the dead-letter queue.
</Warning>

## Prerequisites

* A publicly reachable HTTPS endpoint (HTTP is not accepted; the `targetUrl` must match `^https://.*`).
* A live or sandbox API key.

## Step-by-step webhook setup

<Steps>
  <Step title="Register your webhook endpoint">
    Send `POST /api/v1/enterprise/eudr/webhooks` with the URL of your endpoint and the list of event types to subscribe to.

    **Required fields:**

    | Field        | Type         | Description                                   |
    | ------------ | ------------ | --------------------------------------------- |
    | `targetUrl`  | string (URI) | Your HTTPS callback URL                       |
    | `eventTypes` | string\[]    | Event types to subscribe to (see table below) |

    **Available event types:**

    | Event type            | Fired when                                 |
    | --------------------- | ------------------------------------------ |
    | `polygon.verified`    | A polygon ingestion job completes          |
    | `batch.risk_assessed` | A batch risk assessment finishes           |
    | `shipment.linked`     | A batch–shipment Hedera link is confirmed  |
    | `dds.submitted`       | A DDS is filed to TRACES NT                |
    | `dds.validated`       | TRACES NT confirms the DDS is valid        |
    | `dds.rejected`        | TRACES NT rejects the DDS                  |
    | `job.failed`          | An async job encounters a fatal error      |
    | `report.ready`        | A bulk archive report is ready to download |

    <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:
              webhooks_api = agribackup.EnterpriseWebhooksApi(api_client)
              webhook = webhooks_api.register_webhook(
                  webhook_registration_request={
                      "targetUrl": "https://erp.example.com/agribackup-webhooks",
                      "eventTypes": [
                          "polygon.verified",
                          "batch.risk_assessed",
                          "shipment.linked",
                          "dds.submitted"
                      ]
                  }
              )
              print("Webhook ID:", webhook.webhook_id)
              print("Status:", webhook.status)
              # Store this secret securely — it is shown only once
              print("Signing secret:", webhook.signing_secret)
          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 registerWebhook() {
        try {
          const webhook = await client.webhooks.register({
            targetUrl: 'https://erp.example.com/agribackup-webhooks',
            eventTypes: [
              'polygon.verified',
              'batch.risk_assessed',
              'shipment.linked',
              'dds.submitted'
            ]
          });
          console.log('Webhook ID:', webhook.webhookId);
          console.log('Status:', webhook.status);
          // Store this secret securely — it is shown only once
          console.log('Signing secret:', webhook.signingSecret);
        } catch (error) {
          console.error('Error:', error.message);
        }
      }

      registerWebhook();
      ```

      ```bash cURL theme={null}
      curl --request POST \
        --url https://live.agribackup.com/api/v1/enterprise/eudr/webhooks \
        --header 'X-API-Key: sk_live_YOUR_API_KEY' \
        --header 'Content-Type: application/json' \
        --data '{
          "targetUrl": "https://erp.example.com/agribackup-webhooks",
          "eventTypes": [
            "polygon.verified",
            "batch.risk_assessed",
            "shipment.linked",
            "dds.submitted"
          ]
        }'
      ```
    </CodeGroup>

    The response includes your `signingSecret`:

    ```json theme={null}
    {
      "webhookId": "wh_12345",
      "status": "REGISTERED",
      "targetUrl": "https://erp.example.com/agribackup-webhooks",
      "signingSecret": "whsec_0123456789abcdef"
    }
    ```

    <Warning>
      The `signingSecret` is returned **only once** at registration time. Copy it to your secrets manager immediately. If you lose it, rotate the secret (see Step 3).
    </Warning>
  </Step>

  <Step title="Verify the HMAC-SHA256 signature on every delivery">
    AgriBackup signs every webhook payload by computing `HMAC-SHA256(raw_request_body, signing_secret)` and setting the result as the `X-AgriBackup-Signature` header. You must verify this signature before processing any event to guard against replay and forgery attacks.

    **Verification algorithm:**

    1. Read the raw (unparsed) request body as bytes.
    2. Compute `HMAC-SHA256(body_bytes, signing_secret)`.
    3. Compare your computed digest to the value in the `X-AgriBackup-Signature` header using a constant-time comparison.
    4. Reject the request with `401` if the signatures do not match.

    <CodeGroup>
      ```python Python theme={null}
      import hmac
      import hashlib
      from flask import Flask, request, abort

      app = Flask(__name__)
      SIGNING_SECRET = "whsec_0123456789abcdef"  # Load from your secrets manager

      @app.route("/agribackup-webhooks", methods=["POST"])
      def handle_webhook():
          # 1. Read the raw body before any JSON parsing
          raw_body = request.get_data()

          # 2. Compute expected signature
          expected_sig = hmac.new(
              SIGNING_SECRET.encode("utf-8"),
              raw_body,
              hashlib.sha256
          ).hexdigest()

          # 3. Compare with constant-time comparison
          received_sig = request.headers.get("X-AgriBackup-Signature", "")
          if not hmac.compare_digest(expected_sig, received_sig):
              abort(401, "Invalid signature")

          # 4. Parse and handle the event
          event = request.get_json()
          handle_event(event)

          return "", 200


      def handle_event(event):
          event_type = event.get("eventType")
          if event_type == "polygon.verified":
              polygon_ids = event["data"]["polygonIds"]
              print("Verified polygon IDs:", polygon_ids)
          elif event_type == "shipment.linked":
              tx_hash = event["data"]["transactionHash"]
              print("On-chain transaction:", tx_hash)
          elif event_type == "dds.submitted":
              print("DDS filed. Event ID:", event["eventId"])
          elif event_type == "batch.risk_assessed":
              print("Batch risk assessed. Event ID:", event["eventId"])
      ```

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

      const app = express();
      const SIGNING_SECRET = 'whsec_0123456789abcdef'; // Load from your secrets manager

      // Use raw body parser so we can verify the signature
      app.use('/agribackup-webhooks', express.raw({ type: 'application/json' }));

      app.post('/agribackup-webhooks', (req, res) => {
        // 1. Raw body is already a Buffer due to express.raw()
        const rawBody = req.body;

        // 2. Compute expected signature
        const expectedSig = crypto
          .createHmac('sha256', SIGNING_SECRET)
          .update(rawBody)
          .digest('hex');

        // 3. Constant-time comparison
        const receivedSig = req.headers['x-agribackup-signature'] || '';
        const sigBuffer = Buffer.from(receivedSig, 'hex');
        const expectedBuffer = Buffer.from(expectedSig, 'hex');

        if (
          sigBuffer.length !== expectedBuffer.length ||
          !crypto.timingSafeEqual(sigBuffer, expectedBuffer)
        ) {
          return res.status(401).send('Invalid signature');
        }

        // 4. Parse and handle the event
        const event = JSON.parse(rawBody.toString());
        handleEvent(event);

        res.status(200).send('OK');
      });

      function handleEvent(event) {
        switch (event.eventType) {
          case 'polygon.verified':
            console.log('Verified polygon IDs:', event.data.polygonIds);
            break;
          case 'shipment.linked':
            console.log('On-chain transaction:', event.data.transactionHash);
            break;
          case 'dds.submitted':
            console.log('DDS filed. Event ID:', event.eventId);
            break;
          case 'batch.risk_assessed':
            console.log('Batch risk assessed. Event ID:', event.eventId);
            break;
          default:
            console.log('Unhandled event type:', event.eventType);
        }
      }

      app.listen(3000, () => console.log('Webhook server listening on port 3000'));
      ```
    </CodeGroup>

    <Note>
      Always use a **constant-time comparison** function (`hmac.compare_digest` in Python, `crypto.timingSafeEqual` in Node.js) to prevent timing-based signature oracle attacks.
    </Note>
  </Step>

  <Step title="Rotate the webhook signing secret">
    Rotate your webhook secret periodically or immediately if you suspect it has been compromised. Send `POST /api/v1/enterprise/eudr/webhooks/{webhookId}/rotate-secret`.

    <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:
              webhooks_api = agribackup.EnterpriseWebhooksApi(api_client)
              result = webhooks_api.rotate_webhook_secret(webhook_id="wh_12345")
              # Update your secrets manager with the new signing secret
              print("New signing secret:", result.signing_secret)
          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 rotateSecret(webhookId) {
        try {
          const result = await client.webhooks.rotateSecret(webhookId);
          // Update your secrets manager with the new signing secret
          console.log('New signing secret:', result.signingSecret);
        } catch (error) {
          console.error('Error:', error.message);
        }
      }

      rotateSecret('wh_12345');
      ```

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

    <Warning>
      After rotation, all subsequent deliveries use the new secret. Update your verification logic and secrets manager **before** the old secret expires — any in-flight deliveries signed with the old secret will fail verification.
    </Warning>
  </Step>

  <Step title="Handle the dead-letter queue (DLQ)">
    When AgriBackup cannot deliver an event after 5 consecutive attempts — because your endpoint was down, returned a non-2xx response, or timed out — the event moves to the dead-letter queue. You can inspect, replay, or acknowledge DLQ messages at any time.

    **List failed events:**

    ```
    GET /api/v1/enterprise/eudr/webhooks/dlq
    ```

    <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:
              webhooks_api = agribackup.EnterpriseWebhooksApi(api_client)
              dlq = webhooks_api.get_dlq_messages()
              print(f"Failed messages: {dlq.retrieved_count}")
              for msg in dlq.messages:
                  print(f"  [{msg.event_type}] {msg.event_id} — {msg.attempts} attempts")
          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 listDlqMessages() {
        try {
          const dlq = await client.webhooks.getDlqMessages();
          console.log('Failed messages:', dlq.retrievedCount);
          dlq.messages.forEach(msg => {
            console.log(`  [${msg.eventType}] ${msg.eventId} — ${msg.attempts} attempts`);
          });
        } catch (error) {
          console.error('Error:', error.message);
        }
      }

      listDlqMessages();
      ```

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

    **Replay a single failed event** (re-queues it on the primary exchange):

    ```
    POST /api/v1/enterprise/eudr/webhooks/dlq/{eventId}/replay
    ```

    <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:
              webhooks_api = agribackup.EnterpriseWebhooksApi(api_client)
              webhooks_api.replay_dlq_message(event_id="evt_12345")
              print("Event replayed successfully.")
          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 replayDlqMessage(eventId) {
        try {
          await client.webhooks.replayDlqMessage(eventId);
          console.log('Event replayed successfully.');
        } catch (error) {
          console.error('Error:', error.message);
        }
      }

      replayDlqMessage('evt_12345');
      ```

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

    **Acknowledge and permanently delete a DLQ message** (use when you have handled the event through a manual fallback and no longer need redelivery):

    ```
    DELETE /api/v1/enterprise/eudr/webhooks/dlq/{eventId}
    ```

    <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:
              webhooks_api = agribackup.EnterpriseWebhooksApi(api_client)
              webhooks_api.acknowledge_dlq_message(event_id="evt_12345")
              print("Event acknowledged and removed from DLQ.")
          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 acknowledgeDlqMessage(eventId) {
        try {
          await client.webhooks.acknowledgeDlqMessage(eventId);
          console.log('Event acknowledged and removed from DLQ.');
        } catch (error) {
          console.error('Error:', error.message);
        }
      }

      acknowledgeDlqMessage('evt_12345');
      ```

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

    You can also bulk-replay or bulk-acknowledge multiple DLQ messages in a single call using `POST /api/v1/enterprise/eudr/webhooks/dlq/replay-bulk` and `DELETE /api/v1/enterprise/eudr/webhooks/dlq` respectively, passing an array of `eventIds` in the request body.
  </Step>
</Steps>

## Webhook event payload structure

All webhook deliveries share a common envelope regardless of event type:

```json theme={null}
{
  "eventType": "shipment.linked",
  "eventId": "evt_9988776655",
  "timestamp": "2026-06-12T12:00:00Z",
  "attempt": 0,
  "nextRetry": "2026-06-12T12:05:00Z",
  "data": { }
}
```

| Field       | Description                                                |
| ----------- | ---------------------------------------------------------- |
| `eventType` | One of the subscribed event type strings                   |
| `eventId`   | Unique identifier for deduplication                        |
| `timestamp` | ISO-8601 time the event was generated                      |
| `attempt`   | `0` for the first delivery; increments on each retry       |
| `nextRetry` | Scheduled timestamp for the next retry (if delivery fails) |
| `data`      | Event-specific payload object                              |

<Tip>
  Use `eventId` to implement idempotent event handling. Your endpoint may receive the same event more than once during retries — store processed `eventId` values and skip duplicates.
</Tip>

## Managing webhook registrations

List all active registrations with `GET /api/v1/enterprise/eudr/webhooks`. Delete a registration (and stop all future deliveries) with `DELETE /api/v1/enterprise/eudr/webhooks/{webhookId}`.

<Note>
  Signing secrets are **redacted** from the list endpoint response. The full secret is only available at registration time or immediately after a rotation.
</Note>
