# Parcel Radar — Warehouse handling API

สำหรับคลังสินค้าพาร์ทเนอร์: รับเข้า พิมพ์ฉลาก ติดตาม และส่งต่อพัสดุของ Parcel Radar จากระบบคลังสินค้า (WMS) หรือเครื่องสแกนของคุณเอง ด้วย API key ของ Warehouse โดย key ใช้ได้เฉพาะ call ที่ระบุไว้ในหน้านี้เท่านั้น

> **ยังไม่เปิดใช้งาน** API นี้พัฒนาและจัดทำเอกสารไว้แล้ว แต่เส้นทางสาธารณะบนโฮสต์นี้ (`api-sandbox.parcelradar.io`) ยังไม่เปิดใช้งาน ระหว่างนี้ call ที่ส่งไปยัง base URL ด้านล่างจะยังไปไม่ถึง handling API คลังสินค้าพาร์ทเนอร์ขอสิทธิ์เข้าใช้ผ่าน Parcel Radar: ติดต่อเรา แล้วเราจะแจ้งเมื่อ base URL ของคุณพร้อมใช้งาน key ที่คุณสร้างไว้ตอนนี้จะใช้ต่อได้เมื่อเปิดแล้ว [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`

ทุก path ด้านล่างอ้างอิงจาก URL นี้ รับและตอบเป็น JSON (`Content-Type: application/json`) id เป็น UUID แบบ string เวลาเป็น ISO-8601 UTC และวันที่เป็น `yyyy-MM-dd` ส่ง `Accept-Language: th` เพื่อรับข้อความ error เป็นภาษาไทย (ค่าเริ่มต้นเป็นภาษาอังกฤษ)

## การขอสิทธิ์เข้าใช้

1. คลังสินค้าของคุณเป็น Warehouse พาร์ทเนอร์ของ Parcel Radar คือ workspace ของคุณใน Member Portal เป็นประเภท Warehouse ซึ่ง Parcel Radar ตั้งค่าร่วมกับคุณ
2. สมาชิกของ workspace นั้นที่มีสิทธิ์ จัดการ API key สร้าง key ใน Member Portal (API › API keys) และเลือก handling scope ของ key [เปิดหน้า API keys ใน Member Portal](https://member-sandbox.parcelradar.io/api-keys)
3. ระบบของคุณส่ง token ของ key เป็น `Authorization: Bearer <token>` ในทุก call
4. ระหว่างที่เส้นทางสาธารณะยังไม่เปิด ติดต่อเราเพื่อเข้าร่วมและรับแจ้งเมื่อพร้อมใช้งาน: [contact@parcelradar.io](mailto:contact@parcelradar.io)

## API key ของ Warehouse

เฉพาะ key ของ Warehouse เท่านั้นที่มี handling scope ได้ และมีได้เพียง 5 scope นี้ scope กำหนดได้ครั้งเดียวตอนสร้าง key หากต้องการเปลี่ยน ให้ลบ key แล้วสร้างใหม่ แต่ละ workspace มี key ได้ไม่เกิน 10 key และ key กำหนดวันหมดอายุได้

คุณให้ scope ได้เฉพาะ scope ที่ตัวคุณเองมีสิทธิ์ครบ:

| Scope | ทำอะไรได้ | ผู้สร้าง key ต้องมีสิทธิ์ |
| --- | --- | --- |
| `SEARCH` | อ่าน inbox และค้นหาเลขออเดอร์ Member ID หรือเลข tracking | ดูออเดอร์ที่รับดูแล (`HANDLING_VIEW`), รับพัสดุ (`HANDLING_RECEIVE`) |
| `RECEIVE` | รับพัสดุที่รอรับเข้า พร้อมรูปถ่ายและบันทึกความเสียหาย | รับพัสดุ (`HANDLING_RECEIVE`) |
| `LABEL` | พิมพ์ฉลากของออเดอร์ | พิมพ์ฉลาก (`HANDLING_LABEL`) |
| `TRACKING` | อ่านและเขียน checkpoint และ event การติดตามพัสดุในช่วงที่คุณดูแล | ดูออเดอร์ที่รับดูแล (`HANDLING_VIEW`), จัดการการติดตามพัสดุ (`HANDLING_TRACKING`) |
| `HANDOVER` | ดูคลังสินค้าถัดไป ส่งต่อ เรียกคืน ส่งมอบให้ขนส่ง และ line-haul batch | ดูออเดอร์ที่รับดูแล (`HANDLING_VIEW`), ส่งต่อพัสดุ (`HANDLING_HANDOVER`) |

การสร้าง key พร้อม scope อาจตอบ 400 `error.apikey.scopeInvalid` (scope อื่นนอกจาก 5 ตัวนี้) 400 `error.apikey.scopeWarehouseOnly` (workspace ไม่ใช่ Warehouse) หรือ 403 `error.apikey.scopeNotHeld` (scope ที่ต้องใช้สิทธิ์ที่คุณไม่มี)

Handling API อ่าน token ของ key จาก header `Authorization` เท่านั้น ไม่ใช่จาก header `X-API-Key` ที่ Live Rates API ใช้

## สิ่งที่ key ทำได้

ส่ง `Authorization: Bearer <token>` โดยใช้ token ของ key ของ Warehouse ที่มี handling scope ส่วน session ที่ล็อกอินอยู่หรือ one-time token จะถูกปฏิเสธด้วย 401 `error.handling.keyRequired` และจะไม่ถูกนำไปใช้

key ทำงานด้วยสิทธิ์ปัจจุบันของผู้สร้าง จำกัดเฉพาะ scope ของ key และเฉพาะใน workspace ของ key เท่านั้น หากผู้สร้างเสียสิทธิ์ของ call ใด call นั้นจะตอบ 403 `error.permission.denied` หากผู้สร้างเสียสิทธิ์ จัดการ API key หรือออกจาก workspace หรือ workspace ถูกปิดใช้งานหรือรออนุมัติ ทุก call จะตอบ 401 key ที่ถูกลบจะตอบ 401 `error.apikey.revoked` และ key ที่หมดอายุจะตอบ 401 `error.apikey.expired`

ทุก call ทำในนามผู้สร้าง key ให้กับ Warehouse ระบบบันทึก key ไว้กับการรับเข้า การส่งต่อ และการค้นหาทุกครั้ง และ activity ของออเดอร์จะแสดงว่าผู้สร้างทำผ่าน API key

key ไม่เกี่ยวกับเงินและไม่ตั้งสถานะการจัดส่งเอง: ไม่มีรายการค่าใช้จ่าย การชั่งน้ำหนักใหม่ การตัดเงิน การคืนเงินหรือส่วนลด ไม่มีสถานะการจัดส่ง การพักพัสดุ incident การยกเลิกหรือเอกสาร ไม่มีการรับที่ Counter รหัสรับพัสดุ รายงานหรือ statement ซึ่งคนทำใน Member Portal ส่วน handling call ที่ไม่อยู่ในตารางด้านล่างจะตอบ 403 `error.handling.keyNotAllowed` ไม่ว่าผู้สร้างจะมีสิทธิ์อะไรก็ตาม และทุก call ของ key ของ Shop ก็เช่นกัน

## สิ่งที่ edge ยอมรับ

- ให้บริการเฉพาะ path ใต้ `/api/handling/` ที่แต่ละ segment ประกอบด้วยตัวอักษร ตัวเลข `_` และ `-` เท่านั้น นอกนั้น ไม่ว่าจะเป็น path อื่น dot segment ตัวอักษรที่ encode ไว้ หรือ `;` จะตอบ 404 (`error.handling.edgeNotFound` หรือ 404 ของ edge เอง) ส่วน query string ส่งต่อตามเดิม
- call ที่ไม่มี token ของ key ไม่ว่าจะไม่มี header `Authorization` ไม่ใช่ Bearer token หรือเป็น session หรือ one-time token จะตอบ 401 `error.handling.keyRequired` ก่อนขั้นตอนอื่นใด
- แต่ละ IP ของ client ส่งได้เฉลี่ย 50 request ต่อวินาที และ burst ได้สูงสุด 100 เกินกว่านั้น edge จะตอบ 429

## รายการ call

นี่คือทุก call ที่ key ใช้ได้ `{orderId}` `{checkpointId}` `{eventId}` และ `{batchId}` เป็น UUID key ต้องมี scope ของ call และผู้สร้างต้องมีสิทธิ์ตามที่แสดง

| Scope | Method | Path | ผู้สร้าง key ต้องมีสิทธิ์ |
| --- | --- | --- | --- |
| SEARCH | GET | `/inbox` | ดูออเดอร์ที่รับดูแล (`HANDLING_VIEW`) |
| SEARCH | POST | `/search` | รับพัสดุ (`HANDLING_RECEIVE`) |
| RECEIVE | POST | `/receive` (application/json, multipart/form-data) | รับพัสดุ (`HANDLING_RECEIVE`) |
| LABEL | GET | `/orders/{orderId}/label` | พิมพ์ฉลาก (`HANDLING_LABEL`) |
| TRACKING | GET | `/orders/{orderId}/tracking` | ดูออเดอร์ที่รับดูแล (`HANDLING_VIEW`) |
| TRACKING | GET | `/orders/{orderId}/tracking/checkpoints` | ดูออเดอร์ที่รับดูแล (`HANDLING_VIEW`) |
| TRACKING | POST | `/orders/{orderId}/tracking/checkpoints` | จัดการการติดตามพัสดุ (`HANDLING_TRACKING`) |
| TRACKING | PATCH | `/orders/{orderId}/tracking/checkpoints/{checkpointId}` | จัดการการติดตามพัสดุ (`HANDLING_TRACKING`) |
| TRACKING | PUT | `/orders/{orderId}/tracking/checkpoints/{checkpointId}` | จัดการการติดตามพัสดุ (`HANDLING_TRACKING`) |
| TRACKING | DELETE | `/orders/{orderId}/tracking/checkpoints/{checkpointId}` | จัดการการติดตามพัสดุ (`HANDLING_TRACKING`) |
| TRACKING | POST | `/orders/{orderId}/tracking/checkpoints/{checkpointId}/events` | จัดการการติดตามพัสดุ (`HANDLING_TRACKING`) |
| TRACKING | PATCH | `/orders/{orderId}/tracking/checkpoints/{checkpointId}/events/{eventId}` | จัดการการติดตามพัสดุ (`HANDLING_TRACKING`) |
| TRACKING | PUT | `/orders/{orderId}/tracking/checkpoints/{checkpointId}/events/{eventId}` | จัดการการติดตามพัสดุ (`HANDLING_TRACKING`) |
| TRACKING | DELETE | `/orders/{orderId}/tracking/checkpoints/{checkpointId}/events/{eventId}` | จัดการการติดตามพัสดุ (`HANDLING_TRACKING`) |
| HANDOVER | GET | `/next-warehouses` | ส่งต่อพัสดุ (`HANDLING_HANDOVER`) |
| HANDOVER | POST | `/orders/{orderId}/dispatch` | ส่งต่อพัสดุ (`HANDLING_HANDOVER`) |
| HANDOVER | POST | `/orders/{orderId}/recall` | ส่งต่อพัสดุ (`HANDLING_HANDOVER`) |
| HANDOVER | POST | `/orders/{orderId}/release` | ส่งต่อพัสดุ (`HANDLING_HANDOVER`) |
| HANDOVER | GET | `/line-haul-batches` | ดูออเดอร์ที่รับดูแล (`HANDLING_VIEW`) |
| HANDOVER | POST | `/line-haul-batches` | ส่งต่อพัสดุ (`HANDLING_HANDOVER`) |
| HANDOVER | GET | `/line-haul-batches/{batchId}` | ดูออเดอร์ที่รับดูแล (`HANDLING_VIEW`) |
| HANDOVER | POST | `/line-haul-batches/{batchId}/orders` | ส่งต่อพัสดุ (`HANDLING_HANDOVER`) |
| HANDOVER | DELETE | `/line-haul-batches/{batchId}/orders/{orderId}` | ส่งต่อพัสดุ (`HANDLING_HANDOVER`) |
| HANDOVER | POST | `/line-haul-batches/{batchId}/dispatch` | ส่งต่อพัสดุ (`HANDLING_HANDOVER`) |
| HANDOVER | POST | `/line-haul-batches/{batchId}/check-off` | ส่งต่อพัสดุ (`HANDLING_HANDOVER`) + รับพัสดุ (`HANDLING_RECEIVE`) |
| HANDOVER | POST | `/line-haul-batches/{batchId}/close` | ส่งต่อพัสดุ (`HANDLING_HANDOVER`) |

รายละเอียดคำขอ คำตอบ และการปฏิเสธของแต่ละ call อยู่ถัดไป แยกตาม scope (เป็นภาษาอังกฤษ)

## รูปแบบข้อมูล (Shapes)

คำตอบของแต่ละ call ใช้รูปแบบข้อมูลร่วมกันดังนี้

- **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 — inbox และการค้นหา

### GET /inbox

This Warehouse's orders, by tab.

Scope `SEARCH` · ผู้สร้าง key ต้องมีสิทธิ์: ดูออเดอร์ที่รับดูแล (`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`.

**คำตอบ:** **200** `{tab, page, size, total, items: [InboxItem]}`.

**การปฏิเสธ**

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

**ตัวอย่าง**

```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` · ผู้สร้าง key ต้องมีสิทธิ์: รับพัสดุ (`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.

**คำตอบ:** **200** `{termKind (MEMBER_ID|ORDER_NO|TRACKING_NO|EMAIL), items: [InboxItem]}`.

**การปฏิเสธ**

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

**หมายเหตุ**

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

**ตัวอย่าง**

```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 — รับพัสดุเข้า

### POST /receive

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

Scope `RECEIVE` · ผู้สร้าง key ต้องมีสิทธิ์: รับพัสดุ (`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).

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

**การปฏิเสธ**

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

**หมายเหตุ**

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

**ตัวอย่าง**

```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 — พิมพ์ฉลาก

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

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

Scope `LABEL` · ผู้สร้าง key ต้องมีสิทธิ์: พิมพ์ฉลาก (`HANDLING_LABEL`)

**Query**

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

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

**การปฏิเสธ**

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

**ตัวอย่าง**

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

## TRACKING — checkpoint และ event

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

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

Scope `TRACKING` · ผู้สร้าง key ต้องมีสิทธิ์: ดูออเดอร์ที่รับดูแล (`HANDLING_VIEW`)

**คำตอบ:** **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` · ผู้สร้าง key ต้องมีสิทธิ์: ดูออเดอร์ที่รับดูแล (`HANDLING_VIEW`)

**คำตอบ:** **200** `Timeline`.

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

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

Scope `TRACKING` · ผู้สร้าง key ต้องมีสิทธิ์: จัดการการติดตามพัสดุ (`HANDLING_TRACKING`)

**Body**

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

**คำตอบ:** **200** `Timeline` — a replayed `requestId` adds nothing.

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

Change one of your own checkpoints.

Scope `TRACKING` · ผู้สร้าง key ต้องมีสิทธิ์: จัดการการติดตามพัสดุ (`HANDLING_TRACKING`)

**Body**

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

**คำตอบ:** **200** `Timeline`.

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

Remove one of your own checkpoints.

Scope `TRACKING` · ผู้สร้าง key ต้องมีสิทธิ์: จัดการการติดตามพัสดุ (`HANDLING_TRACKING`)

**คำตอบ:** **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` · ผู้สร้าง key ต้องมีสิทธิ์: จัดการการติดตามพัสดุ (`HANDLING_TRACKING`)

**Body**

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

**คำตอบ:** **200** `Timeline`.

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

Change one of your own events.

Scope `TRACKING` · ผู้สร้าง key ต้องมีสิทธิ์: จัดการการติดตามพัสดุ (`HANDLING_TRACKING`)

**Body**

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

**คำตอบ:** **200** `Timeline`.

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

Remove one of your own events.

Scope `TRACKING` · ผู้สร้าง key ต้องมีสิทธิ์: จัดการการติดตามพัสดุ (`HANDLING_TRACKING`)

**คำตอบ:** **200** `Timeline`.

**ทุก call ใน scope นี้อาจตอบ**

- 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 — ส่งต่อ ส่งมอบ และ line-haul batch

### GET /next-warehouses

Where a dispatch of your held leg may go.

Scope `HANDOVER` · ผู้สร้าง key ต้องมีสิทธิ์: ส่งต่อพัสดุ (`HANDLING_HANDOVER`)

**Query**

- `orderId` — required.

**คำตอบ:** **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` · ผู้สร้าง key ต้องมีสิทธิ์: ส่งต่อพัสดุ (`HANDLING_HANDOVER`)

**Body**

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

**คำตอบ:** **200** `LegResult`.

**การปฏิเสธ**

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

**หมายเหตุ**

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

**ตัวอย่าง**

```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` · ผู้สร้าง key ต้องมีสิทธิ์: ส่งต่อพัสดุ (`HANDLING_HANDOVER`)

**Body**

- `{requestId, reason?}`.

**คำตอบ:** **200** `LegResult`.

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

Release the parcel to a carrier.

Scope `HANDOVER` · ผู้สร้าง key ต้องมีสิทธิ์: ส่งต่อพัสดุ (`HANDLING_HANDOVER`)

**Body**

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

**คำตอบ:** **200** `LegResult` — `RELEASED`, outcome `CARRIER`; the carrier's number is then watched.

**การปฏิเสธ**

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

**ตัวอย่าง**

```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` · ผู้สร้าง key ต้องมีสิทธิ์: ดูออเดอร์ที่รับดูแล (`HANDLING_VIEW`)

**Query**

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

**คำตอบ:** **200** `{page, size, total, items: [Batch]}` — rows without `items`.

### POST /line-haul-batches

Open a line-haul batch to another Warehouse.

Scope `HANDOVER` · ผู้สร้าง key ต้องมีสิทธิ์: ส่งต่อพัสดุ (`HANDLING_HANDOVER`)

**Body**

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

**คำตอบ:** **201** `Batch` (`OPEN`).

**การปฏิเสธ**

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

**ตัวอย่าง**

```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` · ผู้สร้าง key ต้องมีสิทธิ์: ดูออเดอร์ที่รับดูแล (`HANDLING_VIEW`)

**คำตอบ:** **200** `Batch` with `items: [{orderId, orderNo, memberId, pieces, declaredWeightKg, state, addedAt, checkedOffAt, receivedAt}]`.

**การปฏิเสธ**

- 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` · ผู้สร้าง key ต้องมีสิทธิ์: ส่งต่อพัสดุ (`HANDLING_HANDOVER`)

**Body**

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

**คำตอบ:** **200** `Batch` — an order already in the batch is skipped, so a retry is safe.

**การปฏิเสธ**

- 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` · ผู้สร้าง key ต้องมีสิทธิ์: ส่งต่อพัสดุ (`HANDLING_HANDOVER`)

**คำตอบ:** **200** `Batch` — an order no longer in the batch answers the batch as it is, so a retry is safe.

**การปฏิเสธ**

- 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` · ผู้สร้าง key ต้องมีสิทธิ์: ส่งต่อพัสดุ (`HANDLING_HANDOVER`)

**Body**

- `{}`.

**คำตอบ:** **200** `Batch` (`DISPATCHED`); a batch already dispatched answers 200 as it is.

**การปฏิเสธ**

- 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` · ผู้สร้าง key ต้องมีสิทธิ์: ส่งต่อพัสดุ (`HANDLING_HANDOVER`) + รับพัสดุ (`HANDLING_RECEIVE`)

**Body**

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

**คำตอบ:** **200** `Batch`; an order already received answers 200 as it is.

**การปฏิเสธ**

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

**หมายเหตุ**

- 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` · ผู้สร้าง key ต้องมีสิทธิ์: ส่งต่อพัสดุ (`HANDLING_HANDOVER`)

**Body**

- `{reason}`.

**คำตอบ:** **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`.

**การปฏิเสธ**

- 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` และการส่งซ้ำ

call ที่สร้างหรือย้ายการดูแลพัสดุรับ `requestId` ซึ่งเป็น string ที่ไม่ซ้ำของคุณเองต่อหนึ่งการกระทำ การส่งซ้ำด้วย `requestId` เดิมจะได้ผลลัพธ์แรกกลับมาและไม่ทำซ้ำ ได้แก่ `/receive` `/dispatch` `/recall` `/release` การเพิ่ม checkpoint การสร้าง batch และการ check-off (สามรายการหลังไม่บังคับ `requestId`) หาก `requestId` ถูกใช้แล้วโดยผู้ดูแลหรือผู้ใช้อื่น จะตอบ 409 `error.request.idConflict` ส่วน call เขียนข้อมูลอื่นไม่รับ `requestId` และส่งซ้ำได้อย่างปลอดภัยตามสถานะ (ตอบ 200 ตามสภาพปัจจุบัน) ได้แก่ การเพิ่มออเดอร์ใน batch (ออเดอร์ที่อยู่ใน batch แล้วจะถูกข้าม) การนำออเดอร์ออก (ออเดอร์ที่ไม่อยู่ใน batch แล้วจะได้ batch กลับมา) การ dispatch batch การปิด batch และการ check-off ออเดอร์ที่รับแล้ว การเพิ่ม event และการแก้ไขหรือลบ checkpoint หรือ event ก็ไม่รับ `requestId` เช่นกัน ให้ส่งซ้ำเมื่อแน่ใจว่าครั้งแรกไม่สำเร็จเท่านั้น

## Error

ทุก error อยู่ในรูป `{"code": "error.…", "message": "…"}` โดย message เป็นภาษาตาม `Accept-Language` (อังกฤษหรือไทย)

| Status | Code | เมื่อใด |
| --- | --- | --- |
| 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. |

## ขีดจำกัด

- การค้นหา (`/search`): 60 ครั้งต่อนาทีต่อผู้สร้าง key (การค้นหาของ key นับรวมกับของผู้สร้าง ทั้งตัวบุคคลและทุก key ของเขา) และ 300 ครั้งต่อนาทีต่อ Warehouse โดยไม่มีการล็อก
- Edge: ต่อ IP ของ client เฉลี่ย 50 request ต่อวินาที และ burst ได้สูงสุด 100
- รูปถ่าย: ไม่เกิน 10 รูปต่อการรับเข้า เป็น JPEG หรือ PNG ไม่เกิน 10 MB ต่อรูป
- หน้าของ inbox และ batch: ไม่เกิน 100 แถว batch หนึ่งมีได้ไม่เกิน 500 ออเดอร์

## ยังไม่อยู่ใน API นี้

combined shipment (การรับเข้าด้วยเลข combined shipment ฉลาก การติดตาม การสร้าง การส่งต่อ การส่งมอบ และการเรียกคืน) การเปลี่ยนเส้นทางของออเดอร์นำเข้า (`PUT /orders/{orderId}/path`) และการค้นหาด้วยเลข tracking ของผู้ขายของออเดอร์นำเข้า จะตามมาภายหลัง ปัจจุบัน key จะได้ 403 สำหรับ call เหล่านั้น และการค้นหาของ key จะไม่พบเลข tracking ของผู้ขาย
