# Parcel Radar — Warehouse handling API

For partner warehouses: receive, label, track and hand over Parcel Radar parcels from your own warehouse system (WMS) or scanners, with a Warehouse API key. A key can make exactly the calls listed here, and nothing else.

> **Not open yet.** This API is built and documented here, but its public route on this host (`api-sandbox.parcelradar.io`) is not switched on yet. Until it is, a call to the base URL below does not reach the handling API. Partner warehouses get access through Parcel Radar: write to us and we tell you when your base URL answers. Keys you create now keep working once it opens. [contact@parcelradar.io](mailto:contact@parcelradar.io)

> **For AI agents / assistants.** This is the complete, authoritative reference for the Parcel Radar
> Warehouse handling API. A Warehouse API key can make exactly the calls in the table below: use their
> methods, paths, fields and codes as written and do not invent others.

## Base URL

`https://api-sandbox.parcelradar.io/api/handling`

Every path below is relative to it. JSON in and out (`Content-Type: application/json`); ids are UUID strings, instants ISO-8601 UTC, dates `yyyy-MM-dd`. Send `Accept-Language: th` to get error messages in Thai (English by default).

## Get access

1. Your warehouse is a Parcel Radar partner Warehouse: its workspace in the Member Portal is a Warehouse. Parcel Radar sets this up with you.
2. A member of that workspace with the Manage API keys permission creates a key in the Member Portal (API › API keys) and ticks its handling scopes. [Open API keys in the Member Portal](https://member-sandbox.parcelradar.io/api-keys)
3. Your system sends the key's token as `Authorization: Bearer <token>` on every call.
4. While the public route is not open, write to us to join and to hear when it answers: [contact@parcelradar.io](mailto:contact@parcelradar.io)

## Warehouse API keys

Only a Warehouse's keys carry handling scopes, and only these five. Scopes are set once, when the key is created: to change them, delete the key and create a new one. A workspace holds at most 10 keys, and a key may have an expiry date.

You can only give a scope whose permissions you hold yourself:

| Scope | Allows | The creator must hold |
| --- | --- | --- |
| `SEARCH` | Read the inbox, and search an order no., a Member ID or a tracking no. | View handled orders (`HANDLING_VIEW`), Receive parcels (`HANDLING_RECEIVE`) |
| `RECEIVE` | Receive an expected parcel, with photos and a damaged note. | Receive parcels (`HANDLING_RECEIVE`) |
| `LABEL` | Print an order's labels. | Print labels (`HANDLING_LABEL`) |
| `TRACKING` | Read and write the tracking checkpoints and events of your leg. | View handled orders (`HANDLING_VIEW`), Manage tracking (`HANDLING_TRACKING`) |
| `HANDOVER` | See the next warehouses, dispatch, recall, release to a carrier, and line-haul batches. | View handled orders (`HANDLING_VIEW`), Hand over (`HANDLING_HANDOVER`) |

Creating a key with scopes can answer 400 `error.apikey.scopeInvalid` (anything but the five), 400 `error.apikey.scopeWarehouseOnly` (the workspace is not a Warehouse) or 403 `error.apikey.scopeNotHeld` (a scope needing a permission you lack).

The handling API reads the key's token from the `Authorization` header only — not from the `X-API-Key` header the Live Rates API uses.

## What a key may do

Send `Authorization: Bearer <token>`, the token of a Warehouse's key with handling scopes. A signed-in session or a one-time token is refused with 401 `error.handling.keyRequired` and never used.

A key acts with its creator's current permissions, limited to its scopes, in the key's own workspace only. If the creator loses a call's permission, that call answers 403 `error.permission.denied`. If the creator loses Manage API keys or leaves the workspace, or the workspace becomes inactive or is awaiting approval, every call answers 401. A deleted key answers 401 `error.apikey.revoked`, an expired one 401 `error.apikey.expired`.

Every call acts as the key's creator, for the Warehouse. The key is recorded on each receipt, hand-over and search, and the order's activity shows the creator acting through an API key.

Keys move no money and set no delivery status by hand: no cost lines, re-weighs, captures, refunds or discounts; no delivery statuses, leg holds, incidents, cancels or documents; no Counter claims, collection codes, reports or statements. People do those in the Member Portal. Any handling call not in the table below answers 403 `error.handling.keyNotAllowed`, whatever the creator may do — and so does every call of a Shop's key.

## What the edge accepts

- Only paths below `/api/handling/` whose segments are letters, digits, `_` and `-` are served. Anything else — another path, a dot segment, an encoded character or a `;` — answers 404 (`error.handling.edgeNotFound`, or the edge's own 404). The query string is passed on.
- A call without a key token — no `Authorization` header, not a Bearer token, or a session or one-time token — answers 401 `error.handling.keyRequired` before anything else happens.
- Each client IP may make 50 requests a second on average, in bursts of up to 100; above that the edge answers 429.

## The calls

These are all the calls a key can make. `{orderId}`, `{checkpointId}`, `{eventId}` and `{batchId}` are UUIDs. A key needs the call's scope; its creator needs the permission shown.

| Scope | Method | Path | The creator must hold |
| --- | --- | --- | --- |
| SEARCH | GET | `/inbox` | View handled orders (`HANDLING_VIEW`) |
| SEARCH | POST | `/search` | Receive parcels (`HANDLING_RECEIVE`) |
| RECEIVE | POST | `/receive` (application/json, multipart/form-data) | Receive parcels (`HANDLING_RECEIVE`) |
| LABEL | GET | `/orders/{orderId}/label` | Print labels (`HANDLING_LABEL`) |
| TRACKING | GET | `/orders/{orderId}/tracking` | View handled orders (`HANDLING_VIEW`) |
| TRACKING | GET | `/orders/{orderId}/tracking/checkpoints` | View handled orders (`HANDLING_VIEW`) |
| TRACKING | POST | `/orders/{orderId}/tracking/checkpoints` | Manage tracking (`HANDLING_TRACKING`) |
| TRACKING | PATCH | `/orders/{orderId}/tracking/checkpoints/{checkpointId}` | Manage tracking (`HANDLING_TRACKING`) |
| TRACKING | PUT | `/orders/{orderId}/tracking/checkpoints/{checkpointId}` | Manage tracking (`HANDLING_TRACKING`) |
| TRACKING | DELETE | `/orders/{orderId}/tracking/checkpoints/{checkpointId}` | Manage tracking (`HANDLING_TRACKING`) |
| TRACKING | POST | `/orders/{orderId}/tracking/checkpoints/{checkpointId}/events` | Manage tracking (`HANDLING_TRACKING`) |
| TRACKING | PATCH | `/orders/{orderId}/tracking/checkpoints/{checkpointId}/events/{eventId}` | Manage tracking (`HANDLING_TRACKING`) |
| TRACKING | PUT | `/orders/{orderId}/tracking/checkpoints/{checkpointId}/events/{eventId}` | Manage tracking (`HANDLING_TRACKING`) |
| TRACKING | DELETE | `/orders/{orderId}/tracking/checkpoints/{checkpointId}/events/{eventId}` | Manage tracking (`HANDLING_TRACKING`) |
| HANDOVER | GET | `/next-warehouses` | Hand over (`HANDLING_HANDOVER`) |
| HANDOVER | POST | `/orders/{orderId}/dispatch` | Hand over (`HANDLING_HANDOVER`) |
| HANDOVER | POST | `/orders/{orderId}/recall` | Hand over (`HANDLING_HANDOVER`) |
| HANDOVER | POST | `/orders/{orderId}/release` | Hand over (`HANDLING_HANDOVER`) |
| HANDOVER | GET | `/line-haul-batches` | View handled orders (`HANDLING_VIEW`) |
| HANDOVER | POST | `/line-haul-batches` | Hand over (`HANDLING_HANDOVER`) |
| HANDOVER | GET | `/line-haul-batches/{batchId}` | View handled orders (`HANDLING_VIEW`) |
| HANDOVER | POST | `/line-haul-batches/{batchId}/orders` | Hand over (`HANDLING_HANDOVER`) |
| HANDOVER | DELETE | `/line-haul-batches/{batchId}/orders/{orderId}` | Hand over (`HANDLING_HANDOVER`) |
| HANDOVER | POST | `/line-haul-batches/{batchId}/dispatch` | Hand over (`HANDLING_HANDOVER`) |
| HANDOVER | POST | `/line-haul-batches/{batchId}/check-off` | Hand over (`HANDLING_HANDOVER`) + Receive parcels (`HANDLING_RECEIVE`) |
| HANDOVER | POST | `/line-haul-batches/{batchId}/close` | Hand over (`HANDLING_HANDOVER`) |

Each call's request, answer and refusals follow, by scope.

## Shapes

The answers share these shapes.

- **Custody** `{legId, kind (WAREHOUSE), state (ASSIGNED|HELD|RELEASED|CANCELLED), source, holdKind, dispatched, assignedAt, receivedAt, dispatchedAt, releasedAt, outcome (CARRIER|…|null)}`
- **LegResult** `{orderId, orderNo, status, custody: Custody, firstReceipt (true on the order's first receipt, else null/false), handedOverFrom (the Warehouse that released it to you, or null)}`
- **InboxItem** `{orderId, orderNo, orderStatus, custody: Custody, provider, destinationCountry, pieces, declaredWeightKg, captured, memberId (RDT-…), fromCountry, checkedOutAt, firstMileTrackingNo, origin {kind (CUSTOMER|SHOP|WAREHOUSE|BATCH), name, batchNo, lineHaulNo, at}, waitingOn, measuredWeightKg, stalled, linkedParcels [{parcelId, trackingNo, weightKg, linkedAt}]}`
- **Timeline** `{shipmentId (null), trackingNo, trackingUrl, checkpoints: [{checkpointId, eventType, name, checkpointStatus, publishStatus, managedBy, tracked, watching, courierName, courierTrackingNo, canRename, canDelete, events: [{id, name, detail, occurredAt, timezone, visible, source, media}]}]}`
- **Batch** `{batchId, lineHaulNo, mode, departureDate, etaDate, state (OPEN|DISPATCHED|CLOSED), direction, origin {tenantId, name}, destination {tenantId, name}, createdAt, dispatchedAt, closedAt, closeReason, counts {orders, received, missing, recalled, reassigned}, items?}`

## SEARCH — the inbox and search

### GET /inbox

This Warehouse's orders, by tab.

Scope `SEARCH` · The creator must hold: View handled orders (`HANDLING_VIEW`)

**Query**

- `tab` — `expected` (to receive) | `held` (default) | `awaiting-release` | `history`.
- `q` — an order no. (contains), or exactly an owner's Member ID or a first-mile tracking no. (or a linked unmatched parcel's).
- `page` — from 0; `size` — default 100, at most 100.
- `stalled=true` — only the tab's Stalled rows: held too long in `held`; dispatched weeks ago or past a line-haul ETA in `awaiting-release`.

**Answer:** **200** `{tab, page, size, total, items: [InboxItem]}`.

**Refusals**

- 400 `error.request.invalid` — an unknown tab.

**Example**

```bash
curl "https://api-sandbox.parcelradar.io/api/handling/inbox?tab=expected" \
  -H "Authorization: Bearer <WAREHOUSE_KEY_TOKEN>"
```

### POST /search

Find the orders this Warehouse expects or holds for a scanned or typed term.

Scope `SEARCH` · The creator must hold: Receive parcels (`HANDLING_RECEIVE`)

**Body**

- `{term}` — a Member ID (`RDT-XXXXXX`), an order no. (`RDOR…`; a carton QR's `:CODE` suffix is ignored) or a first-mile tracking no.

**Answer:** **200** `{termKind (MEMBER_ID|ORDER_NO|TRACKING_NO|EMAIL), items: [InboxItem]}`.

**Refusals**

- 400 `error.handling.termRequired`.
- 404 `error.handling.notExpected` — one answer for anything else: no order, no customer data.
- 429 `error.handling.rateLimited` — the search limits (see Limits).

**Notes**

- Every search is logged, with the key, and counts against the search limits.

**Example**

```bash
curl -X POST "https://api-sandbox.parcelradar.io/api/handling/search" \
  -H "Authorization: Bearer <WAREHOUSE_KEY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"term":"<ORDER_NO>"}'
```

## RECEIVE — receive a parcel

### POST /receive

Receive an expected parcel into this Warehouse — as JSON, or as a form with photos.

Scope `RECEIVE` · The creator must hold: Receive parcels (`HANDLING_RECEIVE`)

**Body**

- JSON `{orderId, requestId, damagedNote?}`.
- Or `multipart/form-data` with the fields `orderId`, `requestId`, `damagedNote` (≤ 1,000 characters: it arrived damaged) and up to **10** parts named `photos` (JPEG or PNG, ≤ 10 MB each; the whole request ≤ 105 MB).

**Answer:** **200** `LegResult` — the leg is `HELD`; `handedOverFrom` names the Warehouse that held it before you, when there was one.

**Refusals**

- 400 `error.request.invalid` — no `orderId`.
- 400 `error.handling.photoRequired` / `error.handling.photoInvalid` / `error.handling.photoCount`.
- 404 `error.handling.notFound` — not an order of yours.
- 404 `error.handling.legNotFound` — not waiting to be received here (a lost race included).
- 409 `error.request.idConflict` — the `requestId` was another handler's or user's.
- 403 `error.handling.keyNotAllowed` — an `orderNo` + `code` (a Counter claim) or an `offlineRef` (an offline receipt): people only.

**Notes**

- The FIRST receipt of an import order at a warehouse outside Thailand needs a photo: without one the answer is 400 `error.handling.photoRequired` and nothing is received.
- Photos are stored as the order's condition photos on your leg; the damaged note rides the receipt.

**Example**

```bash
curl -X POST "https://api-sandbox.parcelradar.io/api/handling/receive" \
  -H "Authorization: Bearer <WAREHOUSE_KEY_TOKEN>" \
  -F orderId=<ORDER_ID> \
  -F requestId=rcv-<ORDER_ID>-1 \
  -F "damagedNote=Corner crushed" \
  -F photos=@front.jpg \
  -F photos=@label.jpg
```

## LABEL — print labels

### GET /orders/{orderId}/label

An order's labels, as HTML ready to print.

Scope `LABEL` · The creator must hold: Print labels (`HANDLING_LABEL`)

**Query**

- `sheet` — `MARK` (default) | `HANDOVER` | `CARRIER`.

**Answer:** **200** `{sheets: [{kind, html, widthMm, heightMm, copy, copies, preview, voided}], missing: [field…]}` — never the customer's user id.

**Refusals**

- 400 `error.handling.sheetInvalid`.
- 409 `error.handling.noNextLeg` — `HANDOVER` without a next warehouse.
- 404 `error.handling.notFound`.

**Example**

```bash
curl "https://api-sandbox.parcelradar.io/api/handling/orders/<ORDER_ID>/label?sheet=MARK" \
  -H "Authorization: Bearer <WAREHOUSE_KEY_TOKEN>"
```

## TRACKING — checkpoints and events

### GET /orders/{orderId}/tracking

The order's legs and the events posted on them.

Scope `TRACKING` · The creator must hold: View handled orders (`HANDLING_VIEW`)

**Answer:** **200** `{orderNo, status, legs: [Custody], events: [{key, legId, postedAt}]}`.

### GET /orders/{orderId}/tracking/checkpoints

The order's tracking timeline: checkpoints and their events.

Scope `TRACKING` · The creator must hold: View handled orders (`HANDLING_VIEW`)

**Answer:** **200** `Timeline`.

### POST /orders/{orderId}/tracking/checkpoints

Add a checkpoint of your own, optionally with its first event.

Scope `TRACKING` · The creator must hold: Manage tracking (`HANDLING_TRACKING`)

**Body**

- `{eventType, name, checkpointStatus?, publishStatus?, firstEvent?: {name, detail?, occurredAt, visible?}, requestId?}`.

**Answer:** **200** `Timeline` — a replayed `requestId` adds nothing.

### PATCH, PUT /orders/{orderId}/tracking/checkpoints/{checkpointId}

Change one of your own checkpoints.

Scope `TRACKING` · The creator must hold: Manage tracking (`HANDLING_TRACKING`)

**Body**

- `{name?, checkpointStatus?, publishStatus?}`.

**Answer:** **200** `Timeline`.

### DELETE /orders/{orderId}/tracking/checkpoints/{checkpointId}

Remove one of your own checkpoints.

Scope `TRACKING` · The creator must hold: Manage tracking (`HANDLING_TRACKING`)

**Answer:** **200** `Timeline`.

### POST /orders/{orderId}/tracking/checkpoints/{checkpointId}/events

Add an event — on your own checkpoint or on Parcel Radar's warehouse checkpoint.

Scope `TRACKING` · The creator must hold: Manage tracking (`HANDLING_TRACKING`)

**Body**

- `{name, detail?, occurredAt, visible?}`.

**Answer:** **200** `Timeline`.

### PATCH, PUT /orders/{orderId}/tracking/checkpoints/{checkpointId}/events/{eventId}

Change one of your own events.

Scope `TRACKING` · The creator must hold: Manage tracking (`HANDLING_TRACKING`)

**Body**

- `{name?, detail?, occurredAt?, visible?}`.

**Answer:** **200** `Timeline`.

### DELETE /orders/{orderId}/tracking/checkpoints/{checkpointId}/events/{eventId}

Remove one of your own events.

Scope `TRACKING` · The creator must hold: Manage tracking (`HANDLING_TRACKING`)

**Answer:** **200** `Timeline`.

**Every call of this scope can also answer**

- 403 `error.handling.checkpointNotOwn` — a system, carrier or other handler's checkpoint or event.
- 409 `error.handling.legReleased` — the next warehouse has received the parcel.
- 400 / 409 `error.tracking.*` — `nameRequired`, `timeRequired`, `checkpointTypeInvalid`, `statusInvalid`, `detailTooLong`, `nothingToChange`, `disabled`, `busy`, `paxelRefused`, …
- 404 `error.tracking.*NotFound`.

## HANDOVER — dispatch, release and line-haul batches

### GET /next-warehouses

Where a dispatch of your held leg may go.

Scope `HANDOVER` · The creator must hold: Hand over (`HANDLING_HANDOVER`)

**Query**

- `orderId` — required.

**Answer:** **200** `{items: [{tenantId, name, city, country, isDefault}]}` — the default first; empty when the order cannot be dispatched now.

### POST /orders/{orderId}/dispatch

Dispatch your held leg to the next warehouse.

Scope `HANDOVER` · The creator must hold: Hand over (`HANDLING_HANDOVER`)

**Body**

- `{requestId, nextTenantId?, reasonCode?, note?}` — omit `nextTenantId` for the default; another listed Warehouse needs a `NEXT_CHANGE` `reasonCode`.

**Answer:** **200** `LegResult`.

**Refusals**

- 409 `error.handling.legNotHeld` / `legDispatched` / `legOnHold` / `orderNotMoving` / `wrongDirection` / `nextNotEligible` / `nextAlreadyHandled` / `legInLineHaul` / `disposalPending`.
- 400 `error.handling.reasonCode` / `error.handling.nextRequired`.

**Notes**

- A key cannot list the reason codes: the active `NEXT_CHANGE` codes are the ones your staff pick in the Member Portal — ask Parcel Radar for the list.

**Example**

```bash
curl -X POST "https://api-sandbox.parcelradar.io/api/handling/orders/<ORDER_ID>/dispatch" \
  -H "Authorization: Bearer <WAREHOUSE_KEY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"requestId":"dsp-<ORDER_ID>-1"}'
```

### POST /orders/{orderId}/recall

Undo your dispatch before the next warehouse receives the parcel.

Scope `HANDOVER` · The creator must hold: Hand over (`HANDLING_HANDOVER`)

**Body**

- `{requestId, reason?}`.

**Answer:** **200** `LegResult`.

### POST /orders/{orderId}/release

Release the parcel to a carrier.

Scope `HANDOVER` · The creator must hold: Hand over (`HANDLING_HANDOVER`)

**Body**

- `{outcome: "CARRIER", carrierTrackingNo, requestId}` — a key releases to a carrier only.

**Answer:** **200** `LegResult` — `RELEASED`, outcome `CARRIER`; the carrier's number is then watched.

**Refusals**

- 400 `error.handling.outcomeRequired` / `error.handling.carrierNoRequired`.
- 409 `error.handling.carrierBeforeCapture` — the order must be captured first (a person captures it on the web).
- 409 `error.handling.legDispatched` / `error.handling.legInLineHaul`.
- 403 `error.handling.keyNotAllowed` — any outcome but `CARRIER`.

**Example**

```bash
curl -X POST "https://api-sandbox.parcelradar.io/api/handling/orders/<ORDER_ID>/release" \
  -H "Authorization: Bearer <WAREHOUSE_KEY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"outcome":"CARRIER","carrierTrackingNo":"<CARRIER_TRACKING_NO>","requestId":"rel-<ORDER_ID>-1"}'
```

### GET /line-haul-batches

Your line-haul batches, newest first.

Scope `HANDOVER` · The creator must hold: View handled orders (`HANDLING_VIEW`)

**Query**

- `direction` — `OUTGOING` | `INCOMING`; `state` — `OPEN` | `DISPATCHED` | `CLOSED`.
- `page` — from 0; `size` — default 20, at most 100.

**Answer:** **200** `{page, size, total, items: [Batch]}` — rows without `items`.

### POST /line-haul-batches

Open a line-haul batch to another Warehouse.

Scope `HANDOVER` · The creator must hold: Hand over (`HANDLING_HANDOVER`)

**Body**

- `{lineHaulNo, mode (AIR|SEA|ROAD), departureDate, etaDate, destinationTenantId, requestId?}` — `lineHaulNo` is 1–60 of `A–Z a–z 0–9 space . / _ -`.

**Answer:** **201** `Batch` (`OPEN`).

**Refusals**

- 400 `error.handling.lineHaulNo` / `error.handling.lineHaulMode` / `error.handling.lineHaulDates` — the number, the mode, or the dates (ETA before departure); 400 `error.handling.nextRequired` — no destination.
- 409 `error.handling.sameHandler` — the destination is this Warehouse; 409 `error.handling.nextNotEligible` — not a Warehouse a batch may go to.
- 409 `error.request.idConflict` — the `requestId` was another handler's or user's.

**Example**

```bash
curl -X POST "https://api-sandbox.parcelradar.io/api/handling/line-haul-batches" \
  -H "Authorization: Bearer <WAREHOUSE_KEY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"lineHaulNo":"TG621 AWB 217-12345675","mode":"AIR","departureDate":"2026-10-05","etaDate":"2026-10-06","destinationTenantId":"<WAREHOUSE_ID>","requestId":"lh-2026-10-05-1"}'
```

### GET /line-haul-batches/{batchId}

One batch with its orders.

Scope `HANDOVER` · The creator must hold: View handled orders (`HANDLING_VIEW`)

**Answer:** **200** `Batch` with `items: [{orderId, orderNo, memberId, pieces, declaredWeightKg, state, addedAt, checkedOffAt, receivedAt}]`.

**Refusals**

- 404 `error.handling.lineHaulNotFound` — not a batch of this Warehouse (as origin or destination).

### POST /line-haul-batches/{batchId}/orders

Add orders to an open batch — all or none.

Scope `HANDOVER` · The creator must hold: Hand over (`HANDLING_HANDOVER`)

**Body**

- `{orderIds: [..]}` or `{orderId}`.

**Answer:** **200** `Batch` — an order already in the batch is skipped, so a retry is safe.

**Refusals**

- 409 `error.handling.lineHaulRefused` `{refusals: [{orderId, orderNo, code}]}` — e.g. `batchDestination`: the order's default next warehouse is not the batch's destination.
- 409 `error.handling.lineHaulFull` — more than 500 orders (after the refusals check).
- 409 `error.handling.lineHaulNotOpen` — the batch is no longer `OPEN`; 404 `error.handling.lineHaulNotFound`.

### DELETE /line-haul-batches/{batchId}/orders/{orderId}

Take an order out of an open batch.

Scope `HANDOVER` · The creator must hold: Hand over (`HANDLING_HANDOVER`)

**Answer:** **200** `Batch` — an order no longer in the batch answers the batch as it is, so a retry is safe.

**Refusals**

- 409 `error.handling.lineHaulNotOpen` — the batch is no longer `OPEN`; 404 `error.handling.lineHaulNotFound`.

### POST /line-haul-batches/{batchId}/dispatch

Dispatch the batch — each order as by `/orders/{orderId}/dispatch`, all or none.

Scope `HANDOVER` · The creator must hold: Hand over (`HANDLING_HANDOVER`)

**Body**

- `{}`.

**Answer:** **200** `Batch` (`DISPATCHED`); a batch already dispatched answers 200 as it is.

**Refusals**

- 409 `error.handling.lineHaulRefused` — all or none, each refusal listed as for adding orders.
- 409 `error.handling.lineHaulEmpty` — no orders in the batch; 409 `error.handling.lineHaulClosed` — the batch is closed.
- 404 `error.handling.lineHaulNotFound`.

### POST /line-haul-batches/{batchId}/check-off

The destination's receipt of one order of an incoming batch.

Scope `HANDOVER` · The creator must hold: Hand over (`HANDLING_HANDOVER`) + Receive parcels (`HANDLING_RECEIVE`)

**Body**

- `{orderId, requestId?, damagedNote?}`.

**Answer:** **200** `Batch`; an order already received answers 200 as it is.

**Refusals**

- 403 `error.permission.denied` — the key's creator does not hold Receive parcels now; nothing is received.
- 404 `error.handling.lineHaulNotFound` — not an incoming batch of this Warehouse, or still `OPEN`.
- 404 `error.handling.lineHaulItemNotFound` — the order is not in the batch (or was recalled or reassigned).
- 409 `error.handling.legNotFound` — the order's next leg is not this Warehouse's any more.
- The receipt's own refusals — 400 `error.handling.photoRequired` on an import's first receipt abroad.

**Notes**

- A check-off is the order's receipt: the key needs the HANDOVER scope, and its creator must hold Receive parcels (`HANDLING_RECEIVE`) as well as Hand over — a key made by someone without Receive parcels cannot check off.
- An import's first receipt abroad cannot carry a photo here: receive it with `/receive`.

### POST /line-haul-batches/{batchId}/close

Close a batch.

Scope `HANDOVER` · The creator must hold: Hand over (`HANDLING_HANDOVER`)

**Body**

- `{reason}`.

**Answer:** **200** `Batch` (`CLOSED`). An `OPEN` batch: the origin discards it. A `DISPATCHED` batch: the destination closes it, a reason required, and every order not received becomes `MISSING`.

**Refusals**

- 400 `error.handling.lineHaulCloseReason` — a `DISPATCHED` batch closed without a reason.
- 409 `error.handling.lineHaulCloseAtDestination` — only the destination closes a `DISPATCHED` batch.
- 404 `error.handling.lineHaulNotFound`.

## `requestId` and retries

Writes that create or move custody take a `requestId`: your own unique string for each action you intend. A retry with the same `requestId` returns the FIRST result and does nothing twice — `/receive`, `/dispatch`, `/recall`, `/release`, a checkpoint add, a batch create and a check-off (`requestId` optional on the last three). A `requestId` already used by another handler or user answers 409 `error.request.idConflict`. The other writes take none and are safe to repeat by their state (200 as it is): adding orders to a batch (an order already in it is skipped), removing one (an order no longer in it answers the batch), a batch dispatch, a batch close and the check-off of an order already received. Adding an event, and changing or removing a checkpoint or an event, take no `requestId` either: repeat one only if you know the first one failed.

## Errors

Every error is `{"code": "error.…", "message": "…"}`, the message in the `Accept-Language` language (English or Thai).

| Status | Code | When |
| --- | --- | --- |
| 401 | `error.handling.keyRequired` | No token, not a Bearer token, or not an API key's token (a signed-in session or a one-time token) — checked before anything else. |
| 401 | `error.token.invalid` | The token does not verify, or names another workspace or user than its key. |
| 401 | `error.apikey.revoked / expired / creatorInactive / workspaceInactive / workspacePendingApproval` | The key was deleted or has expired; its creator lost Manage API keys or left; the workspace is inactive or awaiting approval. |
| 403 | `error.handling.keyNotAllowed` | A call outside the key's scopes, any handling call not listed here, a Shop's key, or a refused variant (a claim or offline receipt, a release that is not to a carrier). |
| 403 | `error.permission.denied` | The key's creator no longer holds the call's permission. |
| 404 | `error.handling.edgeNotFound` | A path the edge does not serve (see What the edge accepts). |
| 404 | `error.handling.notFound` | Not an order of this Warehouse. |
| 400 / 404 / 409 | the call's own codes | Listed with each call. |
| 429 | `error.handling.rateLimited` | The search limits. |
| 429 | (from the edge) | The per-client limit. |

## Limits

- Searches (`/search`): 60 a minute per key creator — a key's searches count against its creator's own, the person's and all their keys' together — and 300 a minute per Warehouse. No lockout.
- The edge: per client IP, 50 requests a second on average, in bursts of up to 100.
- Photos: at most 10 per receipt, JPEG or PNG, at most 10 MB each.
- Inbox and batch pages: at most 100 rows. A batch: at most 500 orders.

## Not in this API yet

Combined shipments (receiving by combined shipment no., their labels, tracking, create, dispatch, release and recall), an import order's route change (`PUT /orders/{orderId}/path`) and a key search matching an import order's seller tracking no. come later. Today a key gets 403 on those calls, and its search does not match a seller's tracking no.
