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
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 |
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).
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:
- Read the raw (unparsed) request body as bytes.
- Compute
HMAC-SHA256(body_bytes, signing_secret).
- Compare your computed digest to the value in the
X-AgriBackup-Signature header using a constant-time comparison.
- 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.
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.
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": { }
}
| 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 |
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.