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

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:
FieldTypeDescription
batchIdstringThe EUDR batch ID to lock
shipmentIdstringShipment reference or logistics tracking ID
Optional fields:
FieldTypeDescription
billOfLadingstringBill of Lading document number, e.g. "BOL-99281744"
vesselNamestringShipping 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.
2

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

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