# POST /partner/dispatch/shipments/{shipment_id}/units

The logistics scan: allocate up to 1000 units to a B2B order, one result per unit. Requires the dispatch:write scope.

Source: https://docs.sealtrust.io/en/reference/post-partner-dispatch-shipments-id-units/

---

This is the logistics scan. You send the units the picker has just put into
the box, up to 1000 per call, and each one gets its own result. A call with 1000
units of which 3 are wrong records the 997 others and names the 3.
The whole flow is in the guide [Tie each unit to its order](/en/expedition-b2b/).

This call can be replayed: a unit already in the order answers
`already_in_shipment`, never an error, and never a second row.

## Authorization

API key in the `Authorization` header, `Bearer` format, with the
`dispatch:write` scope. A key without it gets a 403 whose message names the
missing scope. The plan of the key's brand must include API access, and the
shipment's brand must be on the Compliance + Identity offer.

```http
Authorization: Bearer votre_clef
```

## Rate limit

The same as the other partner routes: one counter per key and one for the sum
of your brand's keys, 120 calls per 60-second window by default. Every call that
writes takes 1 unit of the key's daily quota, whatever the number of units it
carries.

## Request body

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `units` | `array` of `string` | yes | 1 to 1000 units, each as the scanner read it: the 12-character serial printed on the label, the unit link (`https://sealtrust.io/p/{serial}`), its GS1 Digital Link (`.../01/{gtin}/21/{serial}`) or its signed QR link. |
| `reallocate` | `boolean` | no | `true` moves into this order the units that are in another order of your brand. Absent or `false`, such a unit answers `already_allocated`. |
| `reason` | `string` | with `reallocate` | Why the units move, 200 characters at most. It is written on the old row and in the audit log. |

A unit belongs to one active order at a time. The database guarantees it, even
when two pickers scan the same unit in the same second.

## Example request

```bash
curl -i -X POST "https://api.sealtrust.io/v1/partner/dispatch/shipments/318/units" \
  -H "Authorization: Bearer st_live_0000000000000000000000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{"units": ["H897RFWJ4972", "https://sealtrust.io/p/Y5T2VGGF2NP9", "K2M8Q0R4T6V1", "000000000000"]}'
```

## Example response

HTTP 200, even when some units are refused.

```json
{
  "shipment_id": 318,
  "order_ref": "SO-2026-1042",
  "accepted": 2,
  "rejected": 2,
  "units_allocated": 2,
  "results": [
    {"input": "H897RFWJ4972", "status": "allocated", "ok": true, "product_id": 4096, "serial": "H897RFWJ4972", "other_order_ref": null},
    {"input": "https://sealtrust.io/p/Y5T2VGGF2NP9", "status": "allocated", "ok": true, "product_id": 4097, "serial": "Y5T2VGGF2NP9", "other_order_ref": null},
    {"input": "K2M8Q0R4T6V1", "status": "already_allocated", "ok": false, "product_id": 4098, "serial": "K2M8Q0R4T6V1", "other_order_ref": "SO-2026-0998"},
    {"input": "000000000000", "status": "unknown_unit", "ok": false, "product_id": null, "serial": null, "other_order_ref": null}
  ]
}
```

## The result of each unit

| `status` | `ok` | What it means |
| --- | --- | --- |
| `allocated` | `true` | The unit is now in this order. |
| `already_in_shipment` | `true` | It already was. Nothing changed: a second scan or a replayed call has no effect. |
| `reallocated` | `true` | It was in another order, named by `other_order_ref`, and you asked for the move. |
| `unreadable` | `false` | The value is not a unit code: neither a serial nor a unit link. A lot or model link names no unit. |
| `unknown_unit` | `false` | No unit of your brand carries this code. A unit of another brand gets exactly the same answer, so that a code is never confirmed to exist elsewhere. |
| `not_minted` | `false` | The unit exists but is not registered on chain yet. Retry once the mint is confirmed. |
| `already_allocated` | `false` | It is in another order of your brand, named by `other_order_ref`. Remove it from that order, or call again with `reallocate`. |

## Errors

| Code | Condition | What to do |
| --- | --- | --- |
| 401 | Key missing, malformed or unknown. | Send `Authorization: Bearer <your key>` with the whole key. |
| 403 | The key lacks the `dispatch:write` scope. Message `Missing required scopes: dispatch:write`. | Create a key that carries this scope. |
| 403 | The brand is on the Compliance offer. `detail.code` is `OFFER_EXCLUDES_IDENTITY` and `detail.feature` is `dispatch`. Nothing is recorded. | Move to the Compliance + Identity offer. Retrying changes nothing. |
| 429 | Rate limit or daily quota reached. | Wait the number of seconds in `Retry-After`, or midnight UTC for the quota. |
| 404 | No shipment of your brand has this id. A shipment of another brand gets exactly the same answer. | Check the `id` returned at creation. |
| 409 | The shipment is closed or cancelled. `detail.code` is `shipment_not_open`. | A closed shipment no longer changes. Create a new order. |
| 400 | `reallocate` without `reason`. `detail.code` is `reason_required`. | Add a reason. |
| 422 | More than 1000 units, or an invalid body. | Split the batch into calls of 1000 units at most. |
