After a batch clears risk assessment, you lock it to a physical shipment so that your EUDR compliance evidence travels with the cargo. AgriBackup invokes the BATCH_SHIPMENT_GATEKEEPER Hedera smart contract, creating an immutable on-chain record that ties the batch ID to the shipment’s Bill of Lading reference. This on-chain link is what customs authorities and auditors use to verify provenance at port. Because smart-contract execution requires Hedera consensus, the endpoint returns 202 Accepted and emits a shipment.linked webhook when the transaction finalizes.
Only batches with consolidatedRiskState: COMPLIANT can be linked to a shipment. Attempting to link a batch in any other state returns 400 Bad Request.
Prerequisites
- A batch with
consolidatedRiskState: COMPLIANT and its batchId.
- Your logistics shipment reference and Bill of Lading number (BOL).
- A
shipment.linked webhook subscription to receive the confirmation event (see Webhooks Setup).
Link a batch to a shipment
Submit the shipment link request
Send a POST to /api/v1/enterprise/eudr/shipments/link. Include a UUID Idempotency-Key header to make the request safe to retry.Required fields:| Field | Type | Description |
|---|
batchId | string | The EUDR batch ID to lock |
shipmentId | string | Shipment reference or logistics tracking ID |
Optional fields:| Field | Type | Description |
|---|
billOfLading | string | Bill of Lading document number, e.g. "BOL-99281744" |
vesselName | string | Shipping vessel name, e.g. "MSC Gulsun" |
import agribackup
from agribackup.rest import ApiException
import uuid
configuration = agribackup.Configuration()
configuration.api_key['ApiKeyAuth'] = 'sk_live_YOUR_API_KEY'
with agribackup.ApiClient(configuration) as api_client:
try:
logistics_api = agribackup.EnterpriseLogisticsApi(api_client)
job = logistics_api.link_batch_to_shipment(
idempotency_key=str(uuid.uuid4()),
shipment_link_request={
"batchId": "uuid-1234",
"shipmentId": "SHIP-OOCL-9988",
"billOfLading": "BOL-99281744",
"vesselName": "MSC Gulsun"
}
)
print("Job accepted:", job.job_id)
print("Estimated duration (s):", job.estimated_duration_sec)
except ApiException as e:
print("Error:", e)
import { AgriBackupClient } from '@agribackup/sdk';
import { randomUUID } from 'crypto';
const client = new AgriBackupClient({ apiKey: 'sk_live_YOUR_API_KEY' });
async function linkShipment() {
try {
const job = await client.logistics.linkBatchToShipment(
{
batchId: 'uuid-1234',
shipmentId: 'SHIP-OOCL-9988',
billOfLading: 'BOL-99281744',
vesselName: 'MSC Gulsun'
},
{ idempotencyKey: randomUUID() }
);
console.log('Job accepted:', job.jobId);
console.log('Estimated duration (s):', job.estimatedDurationSec);
} catch (error) {
console.error('Error:', error.message);
}
}
linkShipment();
curl --request POST \
--url https://live.agribackup.com/api/v1/enterprise/eudr/shipments/link \
--header 'X-API-Key: sk_live_YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: 550e8400-e29b-41d4-a716-446655440002' \
--data '{
"batchId": "uuid-1234",
"shipmentId": "SHIP-OOCL-9988",
"billOfLading": "BOL-99281744",
"vesselName": "MSC Gulsun"
}'
A successful 202 Accepted response:{
"jobId": "job_334455",
"status": "PENDING",
"estimatedDurationSec": 5,
"createdAt": "2026-07-08T10:15:00Z"
}
Use the Location response header (/api/v1/enterprise/eudr/jobs/job_334455) to poll job progress. Receive the shipment.linked webhook event
When Hedera consensus finalizes, AgriBackup fires a shipment.linked event to your registered webhook endpoint. The payload includes the immutable Hedera transaction ID that serves as the on-chain proof:{
"eventType": "shipment.linked",
"eventId": "evt_9988776655",
"timestamp": "2026-07-08T10:15:30Z",
"attempt": 0,
"data": {
"batchId": "uuid-1234",
"shipmentReference": "SHIP-OOCL-9988",
"transactionHash": "0.0.12345@1234567890.000000000"
}
}
Store the transactionHash in your ERP — it is the authoritative audit record for regulatory inspections. Verify the on-chain link (optional)
Query the cryptographic evidence ledger to confirm the Hedera anchor at any time by calling GET /api/v1/enterprise/eudr/evidence/ledger/shipment/{shipmentId}.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:
evidence_api = agribackup.EnterpriseEvidenceApi(api_client)
evidence = evidence_api.get_shipment_evidence(shipment_id="SHIP-OOCL-9988")
print("Hedera transaction:", evidence.hedera_transaction_id)
print("Anchored at:", evidence.consensus_timestamp)
except ApiException as e:
print("Error:", e)
import { AgriBackupClient } from '@agribackup/sdk';
const client = new AgriBackupClient({ apiKey: 'sk_live_YOUR_API_KEY' });
async function getEvidence(shipmentId) {
try {
const evidence = await client.evidence.getShipmentEvidence(shipmentId);
console.log('Hedera transaction:', evidence.hederaTransactionId);
console.log('Anchored at:', evidence.consensusTimestamp);
} catch (error) {
console.error('Error:', error.message);
}
}
getEvidence('SHIP-OOCL-9988');
curl --request GET \
--url https://live.agribackup.com/api/v1/enterprise/eudr/evidence/ledger/shipment/SHIP-OOCL-9988 \
--header 'X-API-Key: sk_live_YOUR_API_KEY'
Unlink a batch from a shipment (rollback)
If a shipment is cancelled, rerouted, or the wrong batch was linked, you can roll back the Hedera smart-contract lock. Send a POST to /api/v1/enterprise/eudr/shipments/unlink with the batchId and shipmentId. This asynchronously executes the rollback transaction on Hedera, freeing the batch for reassignment to a different shipment.
Unlinking is a blockchain operation and produces a new, permanent Hedera transaction record. You cannot erase the original link from the ledger — the rollback transaction is appended as a separate entry. Keep this in mind for regulatory audit trails.
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:
logistics_api = agribackup.EnterpriseLogisticsApi(api_client)
result = logistics_api.unlink_shipment(
agribackup.ShipmentUnlinkRequest(
batch_id="uuid-1234",
shipment_id="SHIP-OOCL-9988"
)
)
print("Rollback initiated:", result)
except ApiException as e:
print("Error:", e)
import { AgriBackupClient } from '@agribackup/sdk';
const client = new AgriBackupClient({ apiKey: 'sk_live_YOUR_API_KEY' });
async function unlinkShipment(batchId, shipmentId) {
try {
const result = await client.logistics.unlinkShipment({ batchId, shipmentId });
console.log('Rollback initiated:', result);
} catch (error) {
console.error('Error:', error.message);
}
}
unlinkShipment('uuid-1234', 'SHIP-OOCL-9988');
curl --request POST \
--url https://live.agribackup.com/api/v1/enterprise/eudr/shipments/unlink \
--header 'X-API-Key: sk_live_YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"batchId": "uuid-1234",
"shipmentId": "SHIP-OOCL-9988"
}'
A successful rollback request is accepted asynchronously and returns 202 Accepted. The shipment.linked event will not fire again; monitor the job via the returned jobId.
After a successful unlink, the batch consolidatedRiskState reverts to allow a fresh POST /api/v1/enterprise/eudr/shipments/link with a new shipment reference.