Skip to main content
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.
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.

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

1

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:
FieldTypeDescription
targetUrlstring (URI)Your HTTPS callback URL
eventTypesstring[]Event types to subscribe to (see table below)
Available event types:
Event typeFired when
polygon.verifiedA polygon ingestion job completes
batch.risk_assessedA batch risk assessment finishes
shipment.linkedA batch–shipment Hedera link is confirmed
dds.submittedA DDS is filed to TRACES NT
dds.validatedTRACES NT confirms the DDS is valid
dds.rejectedTRACES NT rejects the DDS
job.failedAn async job encounters a fatal error
report.readyA bulk archive report is ready to download
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)
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();
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"
    ]
  }'
The response includes your signingSecret:
{
  "webhookId": "wh_12345",
  "status": "REGISTERED",
  "targetUrl": "https://erp.example.com/agribackup-webhooks",
  "signingSecret": "whsec_0123456789abcdef"
}
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).
2

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.
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"])
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'));
Always use a constant-time comparison function (hmac.compare_digest in Python, crypto.timingSafeEqual in Node.js) to prevent timing-based signature oracle attacks.
3

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.
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)
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');
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'
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.
4

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
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)
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();
curl --request GET \
  --url https://live.agribackup.com/api/v1/enterprise/eudr/webhooks/dlq \
  --header 'X-API-Key: sk_live_YOUR_API_KEY'
Replay a single failed event (re-queues it on the primary exchange):
POST /api/v1/enterprise/eudr/webhooks/dlq/{eventId}/replay
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)
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');
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'
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}
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)
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');
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'
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.

Webhook event payload structure

All webhook deliveries share a common envelope regardless of event type:
{
  "eventType": "shipment.linked",
  "eventId": "evt_9988776655",
  "timestamp": "2026-06-12T12:00:00Z",
  "attempt": 0,
  "nextRetry": "2026-06-12T12:05:00Z",
  "data": { }
}
FieldDescription
eventTypeOne of the subscribed event type strings
eventIdUnique identifier for deduplication
timestampISO-8601 time the event was generated
attempt0 for the first delivery; increments on each retry
nextRetryScheduled timestamp for the next retry (if delivery fails)
dataEvent-specific payload object
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.

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}.
Signing secrets are redacted from the list endpoint response. The full secret is only available at registration time or immediately after a rotation.