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

# Link an EUDR Batch to a Shipment via Bill of Lading

> Lock a compliant EUDR batch to a shipment on the Hedera ledger using a Bill of Lading reference, and roll back the smart-contract link when needed.

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.

<Note>
  Only batches with `consolidatedRiskState: COMPLIANT` can be linked to a shipment. Attempting to link a batch in any other state returns `400 Bad Request`.
</Note>

## 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](/guides/webhooks-setup)).

## Link a batch to a shipment

<Steps>
  <Step title="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"`             |

    <CodeGroup>
      ```python Python theme={null}
      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)
      ```

      ```javascript Node.js theme={null}
      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();
      ```

      ```bash cURL theme={null}
      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"
        }'
      ```
    </CodeGroup>

    A successful `202 Accepted` response:

    ```json theme={null}
    {
      "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.
  </Step>

  <Step title="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:

    ```json theme={null}
    {
      "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.
  </Step>

  <Step title="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}`.

    <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:
              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)
      ```

      ```javascript Node.js theme={null}
      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');
      ```

      ```bash cURL theme={null}
      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'
      ```
    </CodeGroup>
  </Step>
</Steps>

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

<Warning>
  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.
</Warning>

<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:
          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)
  ```

  ```javascript Node.js theme={null}
  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');
  ```

  ```bash cURL theme={null}
  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"
    }'
  ```
</CodeGroup>

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

<Tip>
  After a successful unlink, the batch `consolidatedRiskState` reverts to allow a fresh `POST /api/v1/enterprise/eudr/shipments/link` with a new shipment reference.
</Tip>
