ParcelRadar.io
ParcelRadar.ioDeveloper Portal

API.md

ข้อกำหนด API ของ Parcel Radar ฉบับสมบูรณ์ในเอกสาร Markdown เดียว ครบทุก endpoint ฟิลด์ของคำขอและคำตอบ รหัสผลลัพธ์ และตัวอย่าง เขียนไว้สำหรับ AI agent และเครื่องมือต่าง ๆ เป็นข้อความเดียวกับที่ปุ่ม Download API.md บันทึก (เอกสารเป็นภาษาอังกฤษ)

API.md
# Parcel Radar - Live Rates API (v1)

Real-time shipping rates from every supported carrier in a single request. No sign-up required.

> **For AI agents / assistants.** This is the complete, authoritative reference for the Parcel
> Radar **Live Rates API** — real-time international shipping rates from every supported carrier.
> Use it to help a developer integrate the API: follow the endpoints below exactly (methods, full
> URLs, and field names) and do not invent parameters, paths, or response fields. Key facts:
>
> - **Base URL:** `https://api.parcelradar.io`. Production calls go to `/api/partner/v1/…` and require an
>   `X-API-Key` header. The free, key-less sandbox lane has its own host — `https://api-sandbox.parcelradar.io/api/sandbox/v1/…`
>   — and returns the same shape for testing.
> - **Two ways to get rates:** the synchronous `POST /quote` returns every carrier's rate in one
>   call (it blocks up to ~60s); the asynchronous lane (`/inquiry` → poll → `/results`) is
>   non-blocking — follow the **Get live rates (async)** flow under *Instant Quote — Asynchronous*.
> - Rate results are ordered **promoted first, then cheapest first**; providers that produced no rate are listed last. A promoted row carries `promotion` (`ads` or `boost`) — a paid placement, which you should disclose.

## Base URL

Production requests go to `https://api.parcelradar.io`; the free sandbox lane has its own host, `https://api-sandbox.parcelradar.io`.

- **Production (partner):** `https://api.parcelradar.io/api/partner/v1/…` — requires your `X-API-Key`.
- **Sandbox (free preview):** `https://api-sandbox.parcelradar.io/api/sandbox/v1/…` — key-less, same request/response shape.

**Authentication:** Send your API key in the X-API-Key header on the partner lane (/api/partner/...). The free sandbox lane (/api/sandbox/...) is key-less.

## Instant Quote — Synchronous

One request, one response: POST your shipment to /quote and the call blocks until every carrier's rate is in (or a ~60s timeout, which returns 504). The simplest way to integrate — choose this unless you must avoid holding a request open for up to a minute (e.g. serverless/edge functions with short timeouts, or very high concurrency).

### Get live rates (sync)

`POST https://api.parcelradar.io/api/partner/v1/instant-quote/quote`

Submit a shipment and receive the live rate from every supported carrier in a single call. Blocks until the carrier fan-out settles (or ~60s → 504).

**Request**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `origin` | object | ✓ |  |  | Ship-from address. |
| `origin.country` | string | ✓ | ISO-2, e.g. "TH" |  | Trimmed to 2 characters, upper-cased. |
| `origin.city` | string |  |  |  | City name, free text. |
| `origin.zipcode` | string |  |  |  | Postal code. |
| `destination` | object | ✓ |  |  | Ship-to address. |
| `destination.country` | string | ✓ | ISO-2, e.g. "TH" |  | Trimmed to 2 characters, upper-cased. |
| `destination.city` | string |  |  |  | City name, free text. |
| `destination.zipcode` | string |  |  |  | Postal code. |
| `parcels` | array | ✓ |  |  | One entry per parcel. |
| `parcels[].length` | number |  | > 0 | 1 | Length in the dunit. |
| `parcels[].width` | number |  | > 0 | 1 | Width in the dunit. |
| `parcels[].height` | number |  | > 0 | 1 | Height in the dunit. |
| `parcels[].weight` | number |  | > 0 | 1 | Weight in the wunit. |
| `wunit` | string |  | "kg", "lb" | "kg" | Weight unit for the parcels AND the result's `chargeableWeight`. |
| `dunit` | string |  | "cm", "in" | "cm" | Dimension unit. |
| `currency` | string |  | "THB", "USD", "EUR", "GBP" | "THB" | Goods/declared-value currency for customs (the match-engine's goods_currency). The cost is always returned in all four currencies. |
| `value` | number |  | >= 0 | 100 | Declared goods value. |

**Response**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `status` | string | ✓ | "ready", "failed" |  | Overall quote status on a 200 response. If the carrier fan-out doesn't finish within the ~60s poll window the call returns 504 with { status: "timeout" } instead (see response codes). |
| `results` | array | ✓ |  |  | One entry per provider. |
| `results[].providerCode` | string | ✓ |  |  | The carrier/provider code. |
| `results[].id` | uuid | ✓ | UUID |  | Provider id — pass to /conditions and /ratecard. |
| `results[].name` | string |  |  |  | Provider display name. |
| `results[].description` | string |  |  |  | Short provider description. |
| `results[].image` | string |  |  |  | Provider logo URL. |
| `results[].type` | string |  |  |  | Transport type (e.g. "Air Consolidate"). |
| `results[].ok` | boolean | ✓ | true, false |  | false when the provider produced no quotable rate. |
| `results[].cost` | object |  |  |  | Effective cost in all four currencies — the same money shape as the match-engine, each rounded to 2 decimal places. Omitted when ok=false. |
| `results[].cost.thb` | number | ✓ |  |  | Cost in THB. |
| `results[].cost.usd` | number | ✓ |  |  | Cost in USD. |
| `results[].cost.eur` | number | ✓ |  |  | Cost in EUR. |
| `results[].cost.gbp` | number | ✓ |  |  | Cost in GBP. |
| `results[].chargeableWeight` | number |  |  |  | Chargeable weight used for the quote, expressed in `weightUnit` (omitted when ok=false). |
| `results[].weightUnit` | string |  | "kg", "lb" |  | Unit of `chargeableWeight` — echoes the request's `wunit`. |
| `results[].error` | string |  |  |  | Reason when ok=false. |
| `results[].promotion` | string |  | "ads", "boost" |  | Present when this provider paid for placement on this route — disclose it to your users (omitted on ordinary rows and on results created before this field existed). |
| `results[].nonRateService` | boolean |  | true, false |  | true for a service such as customs clearance or packing, whose price is not comparable to a shipping rate. |

**Request example**

```json
{
  "origin": {
    "country": "TH",
    "city": "Bangkok",
    "zipcode": "10240"
  },
  "destination": {
    "country": "US",
    "city": "New York",
    "zipcode": "11373"
  },
  "parcels": [
    {
      "length": 20,
      "width": 15,
      "height": 10,
      "weight": 2
    }
  ],
  "wunit": "kg",
  "dunit": "cm",
  "currency": "THB",
  "value": 1500
}
```

**Response example**

```json
{
  "status": "ready",
  "results": [
    {
      "providerCode": "thaipost-ems",
      "id": "019e2246-1ec2-75be-b8cd-969d9d61dd31",
      "name": "EMS World Package",
      "description": "",
      "image": "https://cdn.parcelradar.io/logos/thaipost.webp",
      "type": "Air",
      "ok": true,
      "cost": {
        "thb": 1240.5,
        "usd": 34.2,
        "eur": 31.1,
        "gbp": 27
      },
      "chargeableWeight": 2,
      "weightUnit": "kg"
    },
    {
      "providerCode": "dhl-express",
      "id": "019e7e93-c965-747a-8ee1-c1ee21adabaa",
      "name": "DHL Express",
      "ok": false,
      "error": "no rate matched courier_code + service"
    }
  ]
}
```

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 200 | OK | Rates returned. status is "ready" (or "failed" if the quote could not be completed); results has one entry per carrier. |
| 400 | Bad Request | The request body failed validation — a missing origin/destination/parcels, an invalid unit or currency, a non-positive dimension/weight, a negative value, or malformed JSON. The error body's fields array lists each offending field. |
| 401 | Unauthorized | The X-API-Key header is missing, invalid, or revoked. Required on the partner lane; the sandbox lane is key-less. |
| 429 | Too Many Requests | Rate limit exceeded. Back off and retry after a short delay. |
| 500 | Internal Server Error | An unexpected error occurred while producing the quote. The response is generic (no internals are leaked); the request is safe to retry. |
| 503 | Service Unavailable | A required upstream (API-key verification or a carrier gateway) was temporarily unavailable. Retry shortly. |
| 504 | Gateway Timeout | The carrier fan-out didn't settle within the ~60s poll window. The body is { status: "timeout" } (no results) — the quote keeps processing server-side, so either retry /quote or switch to the async lane and poll. |

**Error example**

```json
{
  "ok": false,
  "status": 400,
  "error": "request failed validation",
  "fields": [
    {
      "field": "destination",
      "message": "is required"
    },
    {
      "field": "parcels",
      "message": "at least one parcel is required"
    },
    {
      "field": "currency",
      "message": "must be one of THB, USD, EUR, GBP"
    }
  ]
}
```

## Instant Quote — Asynchronous

Non-blocking, in three steps: POST /inquiry to start, GET /inquiry?id= to poll until the status leaves "processing", then GET /results?inquiryId= for the rates. Returns the same rates as the sync lane — choose this when you don't want a long-held request, want to show progressive UI, or run where request timeouts are short.

### Get live rates (async)

Create an inquiry, poll it until it's ready, then fetch the results — the same rates as the synchronous /quote, without holding a request open. Call these three endpoints in order.

```text
Your app  ──►  Parcel Radar API   (request)
Your app  ◄──  Parcel Radar API   (response)

  (1) ──►  POST /inquiry   needs X-API-Key
      ◄──  202 · { inquiryId }   → id to poll

  (2) ──►  GET /inquiry?id={inquiryId}   poll every ~1–2s  [loop]
      ◄──  { status }   → until "ready"

  (3) ──►  GET /results?inquiryId={inquiryId}
      ◄──  { status, results[] }   → promoted first, then cheapest

```

**Steps**

1. `POST /inquiry` — needs X-API-Key; returns `202 · { inquiryId }`.
2. `GET /inquiry?id={inquiryId}` — poll every ~1–2s; returns `{ status }`. Repeat until `status` is `"ready"` (or `"failed"`).
3. `GET /results?inquiryId={inquiryId}` — returns `{ status, results[] }`.

**Polling tips:** Wait ~1–2s between polls (a short exponential backoff is fine) and give up after about 60s — the same ceiling the synchronous /quote uses. Only POST /inquiry needs your X-API-Key; the poll and results calls just need the returned inquiryId.

### Create an inquiry

`POST https://api.parcelradar.io/api/partner/v1/instant-quote/inquiry`

Start an async quote. Returns immediately with an inquiryId to poll — the carrier fan-out runs in the background. Same shipment body as /quote.

**Request**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `origin` | object | ✓ |  |  | Ship-from address. |
| `origin.country` | string | ✓ | ISO-2, e.g. "TH" |  | Trimmed to 2 characters, upper-cased. |
| `origin.city` | string |  |  |  | City name, free text. |
| `origin.zipcode` | string |  |  |  | Postal code. |
| `destination` | object | ✓ |  |  | Ship-to address. |
| `destination.country` | string | ✓ | ISO-2, e.g. "TH" |  | Trimmed to 2 characters, upper-cased. |
| `destination.city` | string |  |  |  | City name, free text. |
| `destination.zipcode` | string |  |  |  | Postal code. |
| `parcels` | array | ✓ |  |  | One entry per parcel. |
| `parcels[].length` | number |  | > 0 | 1 | Length in the dunit. |
| `parcels[].width` | number |  | > 0 | 1 | Width in the dunit. |
| `parcels[].height` | number |  | > 0 | 1 | Height in the dunit. |
| `parcels[].weight` | number |  | > 0 | 1 | Weight in the wunit. |
| `wunit` | string |  | "kg", "lb" | "kg" | Weight unit for the parcels AND the result's `chargeableWeight`. |
| `dunit` | string |  | "cm", "in" | "cm" | Dimension unit. |
| `currency` | string |  | "THB", "USD", "EUR", "GBP" | "THB" | Goods/declared-value currency for customs (the match-engine's goods_currency). The cost is always returned in all four currencies. |
| `value` | number |  | >= 0 | 100 | Declared goods value. |

**Response**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `inquiryId` | uuid | ✓ |  |  | The id to poll and fetch results with. |
| `status` | string | ✓ | "processing" |  | Always "processing" on create. |

**Request example**

```json
{
  "origin": {
    "country": "TH",
    "city": "Bangkok",
    "zipcode": "10240"
  },
  "destination": {
    "country": "US",
    "city": "New York",
    "zipcode": "11373"
  },
  "parcels": [
    {
      "length": 20,
      "width": 15,
      "height": 10,
      "weight": 2
    }
  ],
  "wunit": "kg",
  "dunit": "cm",
  "currency": "THB",
  "value": 1500
}
```

**Response example**

```json
{
  "inquiryId": "019e2246-1ec2-75be-b8cd-969d9d61dd31",
  "status": "processing"
}
```

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 202 | Accepted | Inquiry created. Poll GET /inquiry?id= until the status is no longer "processing", then GET /results?inquiryId=. |
| 400 | Bad Request | The request body failed validation — same rules as /quote. The error body's fields array lists each offending field. |
| 401 | Unauthorized | The X-API-Key header is missing, invalid, or revoked (partner lane). The sandbox lane is key-less. |
| 429 | Too Many Requests | Rate limit exceeded. Back off and retry after a short delay. |
| 503 | Service Unavailable | API-key verification was temporarily unavailable. Retry shortly. |

### Poll inquiry status

`GET https://api.parcelradar.io/api/partner/v1/instant-quote/inquiry`

Check whether an inquiry has finished. Poll every ~1–2s until the status leaves "processing", then fetch the results.

**Query parameters**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `id` | uuid | ✓ | UUID |  | The inquiryId returned by POST /inquiry. |

**Response**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `inquiryId` | uuid | ✓ |  |  | Echoes the polled id. |
| `status` | string | ✓ | "processing", "ready", "failed" |  | Poll until it leaves "processing". |

**Response example**

```json
{
  "inquiryId": "019e2246-1ec2-75be-b8cd-969d9d61dd31",
  "status": "ready"
}
```

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 200 | OK | The current status. |
| 400 | Bad Request | `id` is missing or not a valid UUID. |
| 404 | Not Found | No inquiry with that id (wrong id, or created on the other lane). |

**Error example**

```json
{
  "ok": false,
  "status": 404,
  "error": "no inquiry found with id 00000000-0000-0000-0000-000000000000"
}
```

### Get inquiry results

`GET https://api.parcelradar.io/api/partner/v1/instant-quote/results`

Fetch the per-provider rates for an inquiry. Same results shape as the synchronous /quote response; results fill in once the status is "ready".

**Query parameters**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `inquiryId` | uuid | ✓ | UUID |  | The inquiryId returned by POST /inquiry. |

**Response**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `inquiryId` | uuid | ✓ |  |  | Echoes the inquiryId. |
| `status` | string | ✓ | "processing", "ready", "failed" |  | Overall status; results is populated once ready. |
| `results` | array | ✓ |  |  | One entry per provider. |
| `results[].providerCode` | string | ✓ |  |  | The carrier/provider code. |
| `results[].id` | uuid | ✓ | UUID |  | Provider id — pass to /conditions and /ratecard. |
| `results[].name` | string |  |  |  | Provider display name. |
| `results[].description` | string |  |  |  | Short provider description. |
| `results[].image` | string |  |  |  | Provider logo URL. |
| `results[].type` | string |  |  |  | Transport type (e.g. "Air Consolidate"). |
| `results[].ok` | boolean | ✓ | true, false |  | false when the provider produced no quotable rate. |
| `results[].cost` | object |  |  |  | Effective cost in all four currencies — the same money shape as the match-engine, each rounded to 2 decimal places. Omitted when ok=false. |
| `results[].cost.thb` | number | ✓ |  |  | Cost in THB. |
| `results[].cost.usd` | number | ✓ |  |  | Cost in USD. |
| `results[].cost.eur` | number | ✓ |  |  | Cost in EUR. |
| `results[].cost.gbp` | number | ✓ |  |  | Cost in GBP. |
| `results[].chargeableWeight` | number |  |  |  | Chargeable weight used for the quote, expressed in `weightUnit` (omitted when ok=false). |
| `results[].weightUnit` | string |  | "kg", "lb" |  | Unit of `chargeableWeight` — echoes the request's `wunit`. |
| `results[].error` | string |  |  |  | Reason when ok=false. |
| `results[].promotion` | string |  | "ads", "boost" |  | Present when this provider paid for placement on this route — disclose it to your users (omitted on ordinary rows and on results created before this field existed). |
| `results[].nonRateService` | boolean |  | true, false |  | true for a service such as customs clearance or packing, whose price is not comparable to a shipping rate. |

**Response example**

```json
{
  "inquiryId": "019e2246-1ec2-75be-b8cd-969d9d61dd31",
  "status": "ready",
  "results": [
    {
      "providerCode": "thaipost-ems",
      "id": "019e2246-1ec2-75be-b8cd-969d9d61dd31",
      "name": "EMS World Package",
      "description": "",
      "image": "https://cdn.parcelradar.io/logos/thaipost.webp",
      "type": "Air",
      "ok": true,
      "cost": {
        "thb": 1240.5,
        "usd": 34.2,
        "eur": 31.1,
        "gbp": 27
      },
      "chargeableWeight": 2,
      "weightUnit": "kg"
    },
    {
      "providerCode": "dhl-express",
      "id": "019e7e93-c965-747a-8ee1-c1ee21adabaa",
      "name": "DHL Express",
      "ok": false,
      "error": "no rate matched courier_code + service"
    }
  ]
}
```

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 200 | OK | Status + per-provider results (results may be empty until the status is "ready"). |
| 400 | Bad Request | `inquiryId` is missing or not a valid UUID. |
| 404 | Not Found | No inquiry with that id. |

**Error example**

```json
{
  "ok": false,
  "status": 404,
  "error": "no inquiry found with id 00000000-0000-0000-0000-000000000000"
}
```

## Provider detail

Look up a provider's shipping conditions or rate card by the provider id returned in a quote result.

### Get provider conditions

`GET https://api.parcelradar.io/api/partner/v1/instant-quote/conditions`

The shipping conditions for one provider — incoterms, transit time, service network, features, weight config and route coverage. Pass a provider id from a /quote result.

**Query parameters**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `id` | uuid | ✓ | UUID |  | Provider id (the `id` from a /quote result). |

**Response**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `id` | uuid | ✓ | UUID |  | Provider id. |
| `name` | string |  |  |  | Provider display name. |
| `type` | string |  |  |  | Transport type. |
| `incoterms` | array |  | e.g. "DAP", "DDP" |  | Supported incoterms. |
| `allIn` | boolean |  | true, false |  | All-in (duties/taxes included). |
| `serviceNetwork` | string |  |  |  | Service network. |
| `transitMinDays` | number |  |  |  | Minimum transit days. |
| `transitMaxDays` | number |  |  |  | Maximum transit days. |
| `features` | array |  |  |  | Feature badges. |
| `features[].name` | string |  |  |  | Feature name. |
| `features[].category` | string |  |  |  | Feature category. |
| `features[].icon` | string |  |  |  | Icon key. |
| `weightConfig` | object |  |  |  | Chargeable-weight config. |
| `weightConfig.divisorKg` | number |  |  |  | Volumetric divisor (kg). |
| `weightConfig.divisorLb` | number |  |  |  | Volumetric divisor (lb). |
| `weightConfig.roundingStepKg` | number |  |  |  | Rounding step (kg). |
| `weightConfig.roundingStepLb` | number |  |  |  | Rounding step (lb). |
| `weightConfig.mode` | string |  | "per_piece", "shipment_level" |  | Chargeable-weight mode. |
| `coverage` | object |  |  |  | Route coverage. |
| `coverage.worldwideOrigin` | boolean |  |  |  | Accepts any origin. |
| `coverage.worldwideDestination` | boolean |  |  |  | Ships anywhere. |
| `coverage.origins` | array |  |  |  | Origin ISO-2 codes. |
| `coverage.destinations` | array |  |  |  | Destination ISO-2 codes. |
| `coverage.transitOverrides` | array |  |  |  | Per-country transit overrides. |
| `coverage.transitOverrides[].countryCode` | string |  |  |  | ISO-2 country. |
| `coverage.transitOverrides[].minDays` | number |  |  |  | Min transit days. |
| `coverage.transitOverrides[].maxDays` | number |  |  |  | Max transit days. |
| `conditions` | array |  |  |  | The provider's shipping conditions grouped by category (packaging/labeling, prohibited items, customs, …), ordered for display. |
| `conditions[].category` | string |  |  |  | Category key (e.g. "packaging_labeling"). |
| `conditions[].categoryLabelEn` | string |  |  |  | Category label (EN). |
| `conditions[].categoryLabelTh` | string |  |  |  | Category label (TH). |
| `conditions[].items` | array |  |  |  | Conditions in this category, in order. |
| `conditions[].items[].textEn` | string |  |  |  | Condition text (EN). |
| `conditions[].items[].textTh` | string |  |  |  | Condition text (TH). |
| `conditions[].items[].countriesMode` | string |  | "worldwide", "include", "exclude" |  | Country scope of this condition. |
| `conditions[].items[].applicableCountries` | array |  |  |  | ISO-2 codes when countriesMode is not "worldwide". |

**Response example**

```json
{
  "id": "019e2246-1ec2-75be-b8cd-969d9d61dd31",
  "name": "EMS World Package",
  "type": "Air",
  "incoterms": [
    "DAP"
  ],
  "allIn": false,
  "serviceNetwork": "consolidate",
  "transitMinDays": 5,
  "transitMaxDays": 7,
  "features": [
    {
      "name": "Tracking",
      "category": "service",
      "icon": "truck"
    }
  ],
  "weightConfig": {
    "divisorKg": 5000,
    "divisorLb": 139,
    "roundingStepKg": 0.5,
    "roundingStepLb": 1,
    "mode": "per_piece"
  },
  "coverage": {
    "worldwideOrigin": false,
    "worldwideDestination": true,
    "origins": [
      "TH"
    ],
    "destinations": [
      "US"
    ],
    "transitOverrides": []
  }
}
```

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 200 | OK | The provider's conditions. |
| 400 | Bad Request | `id` is missing or not a valid UUID. |
| 404 | Not Found | No active provider with that id. |
| 503 | Service Unavailable | The pricing snapshot is still warming up; retry shortly. |

**Error example**

```json
{
  "ok": false,
  "status": 404,
  "error": "no provider found with id 00000000-0000-0000-0000-000000000000"
}
```

### Get provider ratecard

`GET https://api.parcelradar.io/api/partner/v1/instant-quote/ratecard`

The rate card for one provider — display flags, banners, and (for manual-rate providers) a static price curve. Pass a provider id from a /quote result.

**Query parameters**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `id` | uuid | ✓ | UUID |  | Provider id (the `id` from a /quote result). |

**Response**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `id` | uuid | ✓ | UUID |  | Provider id. |
| `name` | string |  |  |  | Provider display name. |
| `type` | string |  |  |  | Transport type. |
| `showStartFrom` | boolean |  | true, false |  | Show a "starts from" price label. |
| `hideDeMinimis` | boolean |  | true, false |  | Hide the de-minimis line. |
| `hidePerUnitPrice` | boolean |  | true, false |  | Hide the per-unit price. |
| `bannerLayout` | string |  |  |  | Banner layout. |
| `banners` | array |  |  |  | Banner image URLs. |
| `priceRange` | object |  |  |  | Static price curve (manual providers; omitted for live-API). |
| `priceRange.unit` | string |  | "kg", "lb", "cbm", "girth" |  | Weight/volume unit. |
| `priceRange.min` | number |  |  |  | Lowest price point. |
| `priceRange.max` | number |  |  |  | Highest price point. |
| `priceRange.points` | array |  |  |  | Price curve points. |

**Response example**

```json
{
  "id": "019e2246-1ec2-75be-b8cd-969d9d61dd31",
  "name": "EMS World Package",
  "type": "Air",
  "showStartFrom": false,
  "hideDeMinimis": false,
  "hidePerUnitPrice": false,
  "banners": [],
  "priceRange": {
    "unit": "kg",
    "min": 350,
    "max": 8200,
    "points": []
  }
}
```

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 200 | OK | The provider's rate card. `priceRange` is omitted for live-API providers. |
| 400 | Bad Request | `id` is missing or not a valid UUID. |
| 404 | Not Found | No active provider with that id. |
| 503 | Service Unavailable | The pricing snapshot is still warming up; retry shortly. |

**Error example**

```json
{
  "ok": false,
  "status": 404,
  "error": "no provider found with id 00000000-0000-0000-0000-000000000000"
}
```

## Address Book

Your tenant's saved sender/recipient addresses — the same book members manage in the Member Portal, so changes show up there. Partner lane only. Switching an address off keeps it on file and listed; deleting removes it.

### List address book

`GET https://api.parcelradar.io/api/partner/v1/address-book`

A page of your tenant's saved addresses. Ordering is by stored value, so `country` sorts by ISO code rather than localized name. Switched-off addresses are included by default.

**Query parameters**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `page` | number |  | >= 0 | 0 | Zero-based page index. |
| `size` | number |  | 1–100 | 10 | Page size (clamped to 100). |
| `sort` | string |  | "name", "alias", "country", "city", "phone", "updated" | updated | Column to order by (name and alias are separate). |
| `dir` | string |  | "asc", "desc" | desc | Sort direction. |
| `active` | string |  | "all", "active", "inactive" | all | Filter by the switched-on/off flag. The default returns both — an inactive address is disabled, not hidden. |

**Response**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `items` | array | ✓ |  |  | The saved addresses on this page (empty [] when none). |
| `items[].id` | uuid | ✓ | UUID |  | Address id. |
| `items[].isSender` | boolean | ✓ | true, false |  | Usable as a sender. |
| `items[].isRecipient` | boolean | ✓ | true, false |  | Usable as a recipient. |
| `items[].isActive` | boolean | ✓ | true, false |  | false = switched off: still saved and listed, but meant to be skipped. |
| `items[].alias` | string |  |  |  | Nickname/label. |
| `items[].name` | string | ✓ |  |  | Contact name. |
| `items[].phonePrefix` | string |  |  |  | Dial code as digits, e.g. "66" — render with a leading "+". |
| `items[].phone` | string |  |  |  | National number; the dial code is in phonePrefix. |
| `items[].email` | string |  |  |  | Contact email. |
| `items[].addressLine` | string |  |  |  | Street / address line. |
| `items[].countryCode` | string | ✓ | ISO-2, e.g. "TH" |  | Country code, upper-cased. |
| `items[].postalCode` | string |  |  |  | Postal code. |
| `items[].province` | string |  |  |  | State / Province. |
| `items[].district` | string |  |  |  | County / District. |
| `items[].city` | string |  |  |  | Municipality / City. |
| `items[].createdAt` | string | ✓ |  |  | ISO-8601 creation timestamp. |
| `items[].updatedAt` | string | ✓ |  |  | ISO-8601 last-updated timestamp. |
| `total` | number | ✓ |  |  | Total saved addresses for the tenant (across all pages). |
| `page` | number | ✓ |  |  | The zero-based page index returned. |
| `size` | number | ✓ |  |  | The page size returned. |

**Response example**

```json
{
  "items": [
    {
      "id": "d65e6140-df1c-4581-8885-e79501964f6f",
      "isSender": true,
      "isRecipient": true,
      "isActive": true,
      "alias": "HQ",
      "name": "Vernice West",
      "phonePrefix": "66",
      "phone": "812345678",
      "email": "hq@acme.com",
      "addressLine": "1 Sukhumvit",
      "countryCode": "TH",
      "postalCode": "10110",
      "province": "Bangkok",
      "district": "Watthana",
      "city": "Bangkok",
      "createdAt": "2026-05-01T09:00:00Z",
      "updatedAt": "2026-06-12T14:30:00Z"
    }
  ],
  "total": 1,
  "page": 0,
  "size": 10
}
```

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 200 | OK | A page of the tenant's saved addresses ({ items, total, page, size }). |
| 400 | Bad Request | A query param is invalid — a non-numeric page/size, or a sort/dir/active outside the allowed values. |
| 401 | Unauthorized | The X-API-Key header is missing, invalid, revoked, or expired. |
| 503 | Service Unavailable | API-key verification was temporarily unavailable. Retry shortly. |

**Error example**

```json
{
  "ok": false,
  "status": 401,
  "error": "invalid or revoked API key"
}
```

### Create an address

`POST https://api.parcelradar.io/api/partner/v1/address-book`

Save a new address. At least one role flag must be true; an address can be both, and then appears in both lists. Returns the address as stored.

**Request**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `isSender` | boolean |  | true, false | false | Usable as a sender. At least one role must be true. |
| `isRecipient` | boolean |  | true, false | false | Usable as a recipient. At least one role must be true. |
| `isActive` | boolean |  | true, false | true | Omitted on create = active; omitted on update = keep the current state. |
| `name` | string | ✓ | 1–200 chars |  | Contact name. |
| `countryCode` | string | ✓ | ISO-2, e.g. "TH" |  | Stored upper-cased. |
| `alias` | string |  | ≤ 200 chars |  | Nickname/label. Blank is stored as null. |
| `phonePrefix` | string |  | 1–4 digits, e.g. "66" |  | Dial code, no "+". Needs a phone to go with it; together at most 15 digits (E.164). |
| `phone` | string |  | 4–14 digits |  | National number, e.g. "812345678". The dial code goes in phonePrefix. |
| `email` | string |  | ≤ 200 chars |  | Contact email. |
| `addressLine` | string |  | ≤ 500 chars |  | Street / address line. |
| `postalCode` | string |  | ≤ 12 chars, upper-case |  | Letters and digits, single spaces or hyphens: "10110", "SW1A 1AA", "100-0001". |
| `province` | string |  | ≤ 200 chars |  | State / Province. |
| `district` | string |  | ≤ 200 chars |  | County / District. |
| `city` | string |  | ≤ 200 chars |  | Municipality / City. |

**Response**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `id` | uuid | ✓ | UUID |  | Address id. |
| `isSender` | boolean | ✓ | true, false |  | Usable as a sender. |
| `isRecipient` | boolean | ✓ | true, false |  | Usable as a recipient. |
| `isActive` | boolean | ✓ | true, false |  | false = switched off: still saved and listed, but meant to be skipped. |
| `alias` | string |  |  |  | Nickname/label. |
| `name` | string | ✓ |  |  | Contact name. |
| `phonePrefix` | string |  |  |  | Dial code as digits, e.g. "66" — render with a leading "+". |
| `phone` | string |  |  |  | National number; the dial code is in phonePrefix. |
| `email` | string |  |  |  | Contact email. |
| `addressLine` | string |  |  |  | Street / address line. |
| `countryCode` | string | ✓ | ISO-2, e.g. "TH" |  | Country code, upper-cased. |
| `postalCode` | string |  |  |  | Postal code. |
| `province` | string |  |  |  | State / Province. |
| `district` | string |  |  |  | County / District. |
| `city` | string |  |  |  | Municipality / City. |
| `createdAt` | string | ✓ |  |  | ISO-8601 creation timestamp. |
| `updatedAt` | string | ✓ |  |  | ISO-8601 last-updated timestamp. |

**Request example**

```json
{
  "isSender": true,
  "isRecipient": true,
  "alias": "HQ",
  "name": "Vernice West",
  "phonePrefix": "66",
  "phone": "812345678",
  "email": "hq@acme.com",
  "addressLine": "1 Sukhumvit",
  "countryCode": "TH",
  "postalCode": "10110",
  "province": "Bangkok",
  "district": "Watthana",
  "city": "Bangkok"
}
```

**Response example**

```json
{
  "id": "d65e6140-df1c-4581-8885-e79501964f6f",
  "isSender": true,
  "isRecipient": true,
  "isActive": true,
  "alias": "HQ",
  "name": "Vernice West",
  "phonePrefix": "66",
  "phone": "812345678",
  "email": "hq@acme.com",
  "addressLine": "1 Sukhumvit",
  "countryCode": "TH",
  "postalCode": "10110",
  "province": "Bangkok",
  "district": "Watthana",
  "city": "Bangkok",
  "createdAt": "2026-07-16T09:00:00Z",
  "updatedAt": "2026-07-16T09:00:00Z"
}
```

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 200 | OK | The address was saved; the body is the stored address. |
| 400 | Bad Request | The body failed validation — a missing name/countryCode, no role flag, or a field over its length limit. The `fields` array names each problem. |
| 401 | Unauthorized | The X-API-Key header is missing, invalid, revoked, or expired. |
| 409 | Conflict | You already have an address identical in every field. |
| 503 | Service Unavailable | API-key verification was temporarily unavailable. Retry shortly. |

**Error example**

```json
{
  "ok": false,
  "status": 400,
  "error": "request failed validation",
  "fields": [
    {
      "field": "countryCode",
      "message": "is required"
    }
  ]
}
```

### Update an address

`PUT https://api.parcelradar.io/api/partner/v1/address-book/{id}`

Full replacement (PUT): send the complete address — omitted optional fields clear to null. Omit `isActive` to keep the current on/off state (toggle it with `PATCH /{id}/active`).

**Request**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `isSender` | boolean | ✓ | true, false | false | Usable as a sender. At least one role must be true. Omitted → false (and if isRecipient is also false, 400). |
| `isRecipient` | boolean | ✓ | true, false | false | Usable as a recipient. At least one role must be true. Omitted → false (and if isSender is also false, 400). |
| `isActive` | boolean |  | true, false | keeps current | Omitted on create = active; omitted on update = keep the current state. Change the flag with PATCH /{id}/active. |
| `name` | string | ✓ | 1–200 chars |  | Contact name. Omitted → 400; the address is not written. |
| `countryCode` | string | ✓ | ISO-2, e.g. "TH" |  | Stored upper-cased. Omitted → 400; the address is not written. |
| `alias` | string | ✓ | ≤ 200 chars |  | Nickname/label. Blank is stored as null. Omitted → cleared to null (the stored value is overwritten). |
| `phonePrefix` | string | ✓ | 1–4 digits, e.g. "66" |  | Dial code, no "+". Needs a phone to go with it; together at most 15 digits (E.164). Omitted → cleared to null. |
| `phone` | string | ✓ | 4–14 digits |  | National number, e.g. "812345678". The dial code goes in phonePrefix. Omitted → cleared to null. |
| `email` | string | ✓ | ≤ 200 chars |  | Contact email. Omitted → cleared to null. |
| `addressLine` | string | ✓ | ≤ 500 chars |  | Street / address line. Omitted → cleared to null. |
| `postalCode` | string | ✓ | ≤ 12 chars, upper-case |  | Letters and digits, single spaces or hyphens: "10110", "SW1A 1AA", "100-0001". Omitted → cleared to null. |
| `province` | string | ✓ | ≤ 200 chars |  | State / Province. Omitted → cleared to null. |
| `district` | string | ✓ | ≤ 200 chars |  | County / District. Omitted → cleared to null. |
| `city` | string | ✓ | ≤ 200 chars |  | Municipality / City. Omitted → cleared to null. |

**Response**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `id` | uuid | ✓ | UUID |  | Address id. |
| `isSender` | boolean | ✓ | true, false |  | Usable as a sender. |
| `isRecipient` | boolean | ✓ | true, false |  | Usable as a recipient. |
| `isActive` | boolean | ✓ | true, false |  | false = switched off: still saved and listed, but meant to be skipped. |
| `alias` | string |  |  |  | Nickname/label. |
| `name` | string | ✓ |  |  | Contact name. |
| `phonePrefix` | string |  |  |  | Dial code as digits, e.g. "66" — render with a leading "+". |
| `phone` | string |  |  |  | National number; the dial code is in phonePrefix. |
| `email` | string |  |  |  | Contact email. |
| `addressLine` | string |  |  |  | Street / address line. |
| `countryCode` | string | ✓ | ISO-2, e.g. "TH" |  | Country code, upper-cased. |
| `postalCode` | string |  |  |  | Postal code. |
| `province` | string |  |  |  | State / Province. |
| `district` | string |  |  |  | County / District. |
| `city` | string |  |  |  | Municipality / City. |
| `createdAt` | string | ✓ |  |  | ISO-8601 creation timestamp. |
| `updatedAt` | string | ✓ |  |  | ISO-8601 last-updated timestamp. |

**Request example**

```json
{
  "isSender": true,
  "isRecipient": true,
  "isActive": true,
  "alias": "HQ",
  "name": "Vernice West",
  "phonePrefix": "66",
  "phone": "812345678",
  "email": "hq@acme.com",
  "addressLine": "1 Sukhumvit",
  "countryCode": "TH",
  "postalCode": "10110",
  "province": "Bangkok",
  "district": "Watthana",
  "city": "Bangkok"
}
```

**Response example**

```json
{
  "id": "d65e6140-df1c-4581-8885-e79501964f6f",
  "isSender": true,
  "isRecipient": true,
  "isActive": true,
  "alias": "HQ",
  "name": "Vernice West",
  "phonePrefix": "66",
  "phone": "812345678",
  "email": "hq@acme.com",
  "addressLine": "1 Sukhumvit",
  "countryCode": "TH",
  "postalCode": "10110",
  "province": "Bangkok",
  "district": "Watthana",
  "city": "Bangkok",
  "createdAt": "2026-07-16T09:00:00Z",
  "updatedAt": "2026-07-16T12:05:00Z"
}
```

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 200 | OK | The address was updated; the body is the stored address. |
| 400 | Bad Request | The body failed validation. The `fields` array names each problem. |
| 401 | Unauthorized | The X-API-Key header is missing, invalid, revoked, or expired. |
| 404 | Not Found | No address with that id belongs to your tenant. |
| 409 | Conflict | The edit would make this address identical to another one you saved. |
| 503 | Service Unavailable | API-key verification was temporarily unavailable. Retry shortly. |

**Error example**

```json
{
  "ok": false,
  "status": 404,
  "error": "no address found with that id"
}
```

### Activate / deactivate an address

`PATCH https://api.parcelradar.io/api/partner/v1/address-book/{id}/active`

Switch an address on or off. A soft disable: it stays in the book and still comes back from the list, so it can be switched back on. Idempotent. To remove it for good, use DELETE.

**Request**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `isActive` | boolean | ✓ | true, false |  | true = usable, false = switched off (still saved and still listed). |

**Response**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `id` | uuid | ✓ | UUID |  | Address id. |
| `isSender` | boolean | ✓ | true, false |  | Usable as a sender. |
| `isRecipient` | boolean | ✓ | true, false |  | Usable as a recipient. |
| `isActive` | boolean | ✓ | true, false |  | false = switched off: still saved and listed, but meant to be skipped. |
| `alias` | string |  |  |  | Nickname/label. |
| `name` | string | ✓ |  |  | Contact name. |
| `phonePrefix` | string |  |  |  | Dial code as digits, e.g. "66" — render with a leading "+". |
| `phone` | string |  |  |  | National number; the dial code is in phonePrefix. |
| `email` | string |  |  |  | Contact email. |
| `addressLine` | string |  |  |  | Street / address line. |
| `countryCode` | string | ✓ | ISO-2, e.g. "TH" |  | Country code, upper-cased. |
| `postalCode` | string |  |  |  | Postal code. |
| `province` | string |  |  |  | State / Province. |
| `district` | string |  |  |  | County / District. |
| `city` | string |  |  |  | Municipality / City. |
| `createdAt` | string | ✓ |  |  | ISO-8601 creation timestamp. |
| `updatedAt` | string | ✓ |  |  | ISO-8601 last-updated timestamp. |

**Request example**

```json
{
  "isActive": false
}
```

**Response example**

```json
{
  "id": "d65e6140-df1c-4581-8885-e79501964f6f",
  "isSender": true,
  "isRecipient": true,
  "isActive": false,
  "alias": "HQ",
  "name": "Vernice West",
  "phonePrefix": "66",
  "phone": "812345678",
  "email": "hq@acme.com",
  "addressLine": "1 Sukhumvit",
  "countryCode": "TH",
  "postalCode": "10110",
  "province": "Bangkok",
  "district": "Watthana",
  "city": "Bangkok",
  "createdAt": "2026-07-16T09:00:00Z",
  "updatedAt": "2026-07-16T12:05:00Z"
}
```

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 200 | OK | The flag was set; the body is the stored address. |
| 400 | Bad Request | isActive is missing from the body. |
| 401 | Unauthorized | The X-API-Key header is missing, invalid, revoked, or expired. |
| 404 | Not Found | No address with that id belongs to your tenant. |
| 503 | Service Unavailable | API-key verification was temporarily unavailable. Retry shortly. |

**Error example**

```json
{
  "ok": false,
  "status": 400,
  "error": "request failed validation",
  "fields": [
    {
      "field": "isActive",
      "message": "is required"
    }
  ]
}
```

### Delete an address

`DELETE https://api.parcelradar.io/api/partner/v1/address-book/{id}`

Permanently remove an address. This cannot be undone and the id stops resolving. To keep it on file but unused, PATCH /{id}/active with `isActive: false` instead.

**Response**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `ok` | boolean | ✓ | true |  | Always true on a 200 — the address was deleted. |
| `id` | uuid | ✓ | UUID |  | The id that was deleted, echoed back. |

**Response example**

```json
{
  "ok": true,
  "id": "d65e6140-df1c-4581-8885-e79501964f6f"
}
```

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 200 | OK | The address was deleted; the body echoes { ok: true, id }. |
| 401 | Unauthorized | The X-API-Key header is missing, invalid, revoked, or expired. |
| 404 | Not Found | No address with that id belongs to your tenant. |
| 503 | Service Unavailable | API-key verification was temporarily unavailable. Retry shortly. |

**Error example**

```json
{
  "ok": false,
  "status": 404,
  "error": "no address found with that id"
}
```

## Orders

Your tenant's shipping orders, shared with the Member Portal. Partner lane only.

### List orders

`GET https://api.parcelradar.io/api/partner/v1/orders`

A page of the tenant's orders, newest first.

**Query parameters**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `status` | string |  | "all", "CREATED", "PROCESSING", "IN_TRANSIT", "DELIVERED", "EXCEPTION", "ON_HOLD", "RETURNING", "RETURNED" | all | Lifecycle filter. |
| `q` | string |  |  |  | Free-text match on order no + countries. |
| `page` | number |  | >= 0 | 0 | Zero-based page index. |
| `size` | number |  | 1–100 | 10 | Page size (clamped to 100). |
| `sort` | string |  | "order", "status", "from", "to", "provider", "updated" | updated | Column to order by. |
| `dir` | string |  | "asc", "desc" | desc | Sort direction. |

**Response**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `items` | array | ✓ |  |  | The orders on this page (empty [] when none). |
| `items[].id` | uuid | ✓ |  |  | Order id. |
| `items[].orderNo` | string | ✓ | e.g. "RDOR202607000042" |  | Reference (RDOR + YYYYMM + 6-digit sequence), unique per tenant. |
| `items[].status` | string | ✓ | "CREATED", "PROCESSING", "IN_TRANSIT", "DELIVERED", "EXCEPTION", "ON_HOLD", "RETURNING", "RETURNED" |  | Lifecycle state. |
| `items[].fromCountry` | string | ✓ | ISO-2, e.g. "TH" |  | Origin, upper-cased. |
| `items[].toCountry` | string | ✓ | ISO-2, e.g. "US" |  | Destination, upper-cased. |
| `items[].serviceProvider` | string |  |  |  | Chosen carrier, else null. |
| `items[].route` | object |  |  |  | Route milestone (ship-from / deliver-to). |
| `items[].route.from` | object |  |  |  | Ship-from address. |
| `items[].route.from.name` | string |  |  |  | Contact name. |
| `items[].route.from.alias` | string |  |  |  | Nickname/label. |
| `items[].route.from.isSender` | boolean |  |  |  | Sender role. |
| `items[].route.from.isRecipient` | boolean |  |  |  | Recipient role. |
| `items[].route.from.phonePrefix` | string |  |  |  | Dial code, digits only. |
| `items[].route.from.phoneNumber` | string |  |  |  | National number. |
| `items[].route.from.email` | string |  |  |  | Contact email. |
| `items[].route.from.addressLine` | string |  |  |  | Street / address line. |
| `items[].route.from.countryCode` | string |  | ISO-2 |  | Country. |
| `items[].route.from.postalCode` | string |  |  |  | Postal code. |
| `items[].route.from.province` | string |  |  |  | State / province. |
| `items[].route.from.district` | string |  |  |  | County / district. |
| `items[].route.from.city` | string |  |  |  | City. |
| `items[].route.to` | object |  |  |  | Deliver-to address. |
| `items[].route.to.name` | string |  |  |  | Contact name. |
| `items[].route.to.alias` | string |  |  |  | Nickname/label. |
| `items[].route.to.isSender` | boolean |  |  |  | Sender role. |
| `items[].route.to.isRecipient` | boolean |  |  |  | Recipient role. |
| `items[].route.to.phonePrefix` | string |  |  |  | Dial code, digits only. |
| `items[].route.to.phoneNumber` | string |  |  |  | National number. |
| `items[].route.to.email` | string |  |  |  | Contact email. |
| `items[].route.to.addressLine` | string |  |  |  | Street / address line. |
| `items[].route.to.countryCode` | string |  | ISO-2 |  | Country. |
| `items[].route.to.postalCode` | string |  |  |  | Postal code. |
| `items[].route.to.province` | string |  |  |  | State / province. |
| `items[].route.to.district` | string |  |  |  | County / district. |
| `items[].route.to.city` | string |  |  |  | City. |
| `items[].packages` | object |  |  |  | Packages milestone. |
| `items[].packages.items` | array |  |  |  | The parcels. |
| `items[].packages.items[].id` | string |  |  |  | Line id. |
| `items[].packages.items[].type` | string |  | "box", "pallet", "envelope", "tube", "poly_mail", "container", "bulk_cargo" |  | Packaging type. |
| `items[].packages.items[].description` | string |  |  |  | Goods description. |
| `items[].packages.items[].quantity` | string |  |  |  | Piece count (numeric string). |
| `items[].packages.items[].weight` | string |  |  |  | Weight per piece (numeric string). |
| `items[].packages.items[].weightUnit` | string |  | "kg", "lb" |  | Weight unit. |
| `items[].packages.items[].length` | string |  |  |  | Length (numeric string). |
| `items[].packages.items[].width` | string |  |  |  | Width (numeric string). |
| `items[].packages.items[].height` | string |  |  |  | Height (numeric string). |
| `items[].packages.items[].dimUnit` | string |  | "cm", "in" |  | Dimension unit. |
| `items[].packages.items[].value` | string |  |  |  | Declared value (numeric string). |
| `items[].packages.items[].valueCurrency` | string |  | "THB", "USD", "EUR", "GBP" |  | Declared-value currency. |
| `items[].requireDocument` | boolean |  | true, false |  | Package Documents STATE flag — true = documents required, false = "none required" acknowledged, null = not set. NOT the files (those are `attachments`). |
| `items[].service` | object |  |  |  | Service milestone (chosen rate + config). |
| `items[].service.providerId` | string |  |  |  | Chosen carrier id. |
| `items[].service.providerCode` | string |  |  |  | Carrier code. |
| `items[].service.providerName` | string |  |  |  | Carrier display name. |
| `items[].service.price` | object |  |  |  | Frozen quote captured at save. |
| `items[].service.price.thb` | number |  |  |  | Total price, THB. |
| `items[].service.price.zoneName` | string |  |  |  | Rated zone. |
| `items[].service.price.customsThb` | number |  |  |  | Customs component, THB. |
| `items[].service.price.pickupThb` | number |  |  |  | Pickup component, THB (optional). |
| `items[].service.shipDate` | string |  | YYYY-MM-DD |  | Ship date. |
| `items[].service.pickupGroupId` | string |  |  |  | Pickup-option group (null = provider default). |
| `items[].service.pickupGroupName` | string |  |  |  | Pickup group name. |
| `items[].service.pickupAddress` | string |  |  |  | Pickup address. |
| `items[].service.pickupInstructions` | string |  |  |  | Pickup instructions. |
| `items[].service.options` | array |  |  |  | Selected service-option names (string[]). |
| `items[].service.references` | array |  |  |  | Shipment references (string[]). |
| `items[].service.note` | string |  |  |  | Internal note. |
| `items[].notifications` | object |  |  |  | Notifications milestone. |
| `items[].notifications.sender` | object |  |  |  | Sender notification prefs. |
| `items[].notifications.sender.email` | string |  |  |  | Notified email. |
| `items[].notifications.sender.language` | string |  | "en", "th" |  | Email language. |
| `items[].notifications.sender.options` | array |  | "created", "in_transit", "delivered", "exception" |  | Events to notify on (string[]). |
| `items[].notifications.recipients` | array |  |  |  | Recipient notification prefs. |
| `items[].notifications.recipients[].email` | string |  |  |  | Notified email. |
| `items[].notifications.recipients[].language` | string |  | "en", "th" |  | Email language. |
| `items[].notifications.recipients[].options` | array |  | "created", "in_transit", "delivered", "exception" |  | Events to notify on (string[]). |
| `items[].createdAt` | string | ✓ |  |  | ISO-8601 timestamp. |
| `items[].updatedAt` | string | ✓ |  |  | ISO-8601 timestamp. |
| `items[].attachments` | array |  |  |  | Uploaded document FILES (metadata) — embedded on a single-order GET/PUT, omitted when the order has none. Distinct from `requireDocument` (that is the state flag; these are the files). Manage them via the documents sub-resource below. |
| `items[].attachments[].id` | uuid | ✓ |  |  | Document id. |
| `items[].attachments[].name` | string | ✓ |  |  | Display name. |
| `items[].attachments[].description` | string |  |  |  | Optional note. |
| `items[].attachments[].docType` | string | ✓ | "INVOICE", "RECEIPT", "TAX", "VAT", "PACKING_LIST", "CUSTOMS", "OTHER" |  | Document type. |
| `items[].attachments[].contentType` | string |  |  |  | MIME type as uploaded. |
| `items[].attachments[].sizeBytes` | number | ✓ |  |  | Decoded file size in bytes. |
| `items[].attachments[].createdAt` | string | ✓ |  |  | ISO-8601 timestamp. |
| `total` | number | ✓ |  |  | Total orders for the tenant (across all pages). |
| `page` | number | ✓ |  |  | The zero-based page index returned. |
| `size` | number | ✓ |  |  | The page size returned. |

**Response example**

```json
{
  "items": [
    {
      "id": "3f9a1c62-8d47-4a1e-9b0c-2e5f6a7b8c90",
      "orderNo": "RDOR202607000042",
      "status": "CREATED",
      "fromCountry": "TH",
      "toCountry": "US",
      "serviceProvider": null,
      "route": null,
      "packages": null,
      "requireDocument": null,
      "service": null,
      "notifications": null,
      "createdAt": "2026-07-16T09:00:00Z",
      "updatedAt": "2026-07-16T09:00:00Z"
    }
  ],
  "total": 1,
  "page": 0,
  "size": 10
}
```

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 200 | OK | A page of the tenant's orders ({ items, total, page, size }). |
| 400 | Bad Request | A query param is invalid — a non-numeric page/size, or a status/sort/dir outside the allowed values. |
| 401 | Unauthorized | The X-API-Key header is missing, invalid, revoked, or expired. |
| 503 | Service Unavailable | API-key verification was temporarily unavailable. Retry shortly. |

**Error example**

```json
{
  "ok": false,
  "status": 401,
  "error": "invalid or revoked API key"
}
```

### Create an order

`POST https://api.parcelradar.io/api/partner/v1/orders`

Create an order from the two required routing countries.

**Request**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `fromCountry` | string | ✓ | ISO-2, in the country list |  | Origin. Upper-cased; a code not in the list is rejected (400). |
| `toCountry` | string | ✓ | ISO-2, in the country list |  | Destination. Upper-cased; a code not in the list is rejected (400). |

**Response**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `id` | uuid | ✓ |  |  | Order id. |
| `orderNo` | string | ✓ | e.g. "RDOR202607000042" |  | Reference (RDOR + YYYYMM + 6-digit sequence), unique per tenant. |
| `status` | string | ✓ | "CREATED", "PROCESSING", "IN_TRANSIT", "DELIVERED", "EXCEPTION", "ON_HOLD", "RETURNING", "RETURNED" |  | Lifecycle state. |
| `fromCountry` | string | ✓ | ISO-2, e.g. "TH" |  | Origin, upper-cased. |
| `toCountry` | string | ✓ | ISO-2, e.g. "US" |  | Destination, upper-cased. |
| `serviceProvider` | string |  |  |  | Chosen carrier, else null. |
| `route` | object |  |  |  | Route milestone (ship-from / deliver-to). |
| `route.from` | object |  |  |  | Ship-from address. |
| `route.from.name` | string |  |  |  | Contact name. |
| `route.from.alias` | string |  |  |  | Nickname/label. |
| `route.from.isSender` | boolean |  |  |  | Sender role. |
| `route.from.isRecipient` | boolean |  |  |  | Recipient role. |
| `route.from.phonePrefix` | string |  |  |  | Dial code, digits only. |
| `route.from.phoneNumber` | string |  |  |  | National number. |
| `route.from.email` | string |  |  |  | Contact email. |
| `route.from.addressLine` | string |  |  |  | Street / address line. |
| `route.from.countryCode` | string |  | ISO-2 |  | Country. |
| `route.from.postalCode` | string |  |  |  | Postal code. |
| `route.from.province` | string |  |  |  | State / province. |
| `route.from.district` | string |  |  |  | County / district. |
| `route.from.city` | string |  |  |  | City. |
| `route.to` | object |  |  |  | Deliver-to address. |
| `route.to.name` | string |  |  |  | Contact name. |
| `route.to.alias` | string |  |  |  | Nickname/label. |
| `route.to.isSender` | boolean |  |  |  | Sender role. |
| `route.to.isRecipient` | boolean |  |  |  | Recipient role. |
| `route.to.phonePrefix` | string |  |  |  | Dial code, digits only. |
| `route.to.phoneNumber` | string |  |  |  | National number. |
| `route.to.email` | string |  |  |  | Contact email. |
| `route.to.addressLine` | string |  |  |  | Street / address line. |
| `route.to.countryCode` | string |  | ISO-2 |  | Country. |
| `route.to.postalCode` | string |  |  |  | Postal code. |
| `route.to.province` | string |  |  |  | State / province. |
| `route.to.district` | string |  |  |  | County / district. |
| `route.to.city` | string |  |  |  | City. |
| `packages` | object |  |  |  | Packages milestone. |
| `packages.items` | array |  |  |  | The parcels. |
| `packages.items[].id` | string |  |  |  | Line id. |
| `packages.items[].type` | string |  | "box", "pallet", "envelope", "tube", "poly_mail", "container", "bulk_cargo" |  | Packaging type. |
| `packages.items[].description` | string |  |  |  | Goods description. |
| `packages.items[].quantity` | string |  |  |  | Piece count (numeric string). |
| `packages.items[].weight` | string |  |  |  | Weight per piece (numeric string). |
| `packages.items[].weightUnit` | string |  | "kg", "lb" |  | Weight unit. |
| `packages.items[].length` | string |  |  |  | Length (numeric string). |
| `packages.items[].width` | string |  |  |  | Width (numeric string). |
| `packages.items[].height` | string |  |  |  | Height (numeric string). |
| `packages.items[].dimUnit` | string |  | "cm", "in" |  | Dimension unit. |
| `packages.items[].value` | string |  |  |  | Declared value (numeric string). |
| `packages.items[].valueCurrency` | string |  | "THB", "USD", "EUR", "GBP" |  | Declared-value currency. |
| `requireDocument` | boolean |  | true, false |  | Package Documents STATE flag — true = documents required, false = "none required" acknowledged, null = not set. NOT the files (those are `attachments`). |
| `service` | object |  |  |  | Service milestone (chosen rate + config). |
| `service.providerId` | string |  |  |  | Chosen carrier id. |
| `service.providerCode` | string |  |  |  | Carrier code. |
| `service.providerName` | string |  |  |  | Carrier display name. |
| `service.price` | object |  |  |  | Frozen quote captured at save. |
| `service.price.thb` | number |  |  |  | Total price, THB. |
| `service.price.zoneName` | string |  |  |  | Rated zone. |
| `service.price.customsThb` | number |  |  |  | Customs component, THB. |
| `service.price.pickupThb` | number |  |  |  | Pickup component, THB (optional). |
| `service.shipDate` | string |  | YYYY-MM-DD |  | Ship date. |
| `service.pickupGroupId` | string |  |  |  | Pickup-option group (null = provider default). |
| `service.pickupGroupName` | string |  |  |  | Pickup group name. |
| `service.pickupAddress` | string |  |  |  | Pickup address. |
| `service.pickupInstructions` | string |  |  |  | Pickup instructions. |
| `service.options` | array |  |  |  | Selected service-option names (string[]). |
| `service.references` | array |  |  |  | Shipment references (string[]). |
| `service.note` | string |  |  |  | Internal note. |
| `notifications` | object |  |  |  | Notifications milestone. |
| `notifications.sender` | object |  |  |  | Sender notification prefs. |
| `notifications.sender.email` | string |  |  |  | Notified email. |
| `notifications.sender.language` | string |  | "en", "th" |  | Email language. |
| `notifications.sender.options` | array |  | "created", "in_transit", "delivered", "exception" |  | Events to notify on (string[]). |
| `notifications.recipients` | array |  |  |  | Recipient notification prefs. |
| `notifications.recipients[].email` | string |  |  |  | Notified email. |
| `notifications.recipients[].language` | string |  | "en", "th" |  | Email language. |
| `notifications.recipients[].options` | array |  | "created", "in_transit", "delivered", "exception" |  | Events to notify on (string[]). |
| `createdAt` | string | ✓ |  |  | ISO-8601 timestamp. |
| `updatedAt` | string | ✓ |  |  | ISO-8601 timestamp. |
| `attachments` | array |  |  |  | Uploaded document FILES (metadata) — embedded on a single-order GET/PUT, omitted when the order has none. Distinct from `requireDocument` (that is the state flag; these are the files). Manage them via the documents sub-resource below. |
| `attachments[].id` | uuid | ✓ |  |  | Document id. |
| `attachments[].name` | string | ✓ |  |  | Display name. |
| `attachments[].description` | string |  |  |  | Optional note. |
| `attachments[].docType` | string | ✓ | "INVOICE", "RECEIPT", "TAX", "VAT", "PACKING_LIST", "CUSTOMS", "OTHER" |  | Document type. |
| `attachments[].contentType` | string |  |  |  | MIME type as uploaded. |
| `attachments[].sizeBytes` | number | ✓ |  |  | Decoded file size in bytes. |
| `attachments[].createdAt` | string | ✓ |  |  | ISO-8601 timestamp. |

**Request example**

```json
{
  "fromCountry": "TH",
  "toCountry": "US"
}
```

**Response example**

```json
{
  "id": "3f9a1c62-8d47-4a1e-9b0c-2e5f6a7b8c90",
  "orderNo": "RDOR202607000042",
  "status": "CREATED",
  "fromCountry": "TH",
  "toCountry": "US",
  "serviceProvider": null,
  "route": null,
  "packages": null,
  "requireDocument": null,
  "service": null,
  "notifications": null,
  "createdAt": "2026-07-16T09:00:00Z",
  "updatedAt": "2026-07-16T09:00:00Z"
}
```

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 200 | OK | The order was created; the body is the stored order. |
| 400 | Bad Request | A country is missing or not a known ISO code. |
| 401 | Unauthorized | The X-API-Key header is missing, invalid, revoked, or expired. |
| 503 | Service Unavailable | API-key verification was temporarily unavailable. Retry shortly. |

**Error example**

```json
{
  "ok": false,
  "status": 400,
  "error": "request failed validation",
  "fields": [
    {
      "field": "toCountry",
      "message": "is not a valid country"
    }
  ]
}
```

### Get an order

`GET https://api.parcelradar.io/api/partner/v1/orders/{id}`

Fetch one order by id.

**Response**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `id` | uuid | ✓ |  |  | Order id. |
| `orderNo` | string | ✓ | e.g. "RDOR202607000042" |  | Reference (RDOR + YYYYMM + 6-digit sequence), unique per tenant. |
| `status` | string | ✓ | "CREATED", "PROCESSING", "IN_TRANSIT", "DELIVERED", "EXCEPTION", "ON_HOLD", "RETURNING", "RETURNED" |  | Lifecycle state. |
| `fromCountry` | string | ✓ | ISO-2, e.g. "TH" |  | Origin, upper-cased. |
| `toCountry` | string | ✓ | ISO-2, e.g. "US" |  | Destination, upper-cased. |
| `serviceProvider` | string |  |  |  | Chosen carrier, else null. |
| `route` | object |  |  |  | Route milestone (ship-from / deliver-to). |
| `route.from` | object |  |  |  | Ship-from address. |
| `route.from.name` | string |  |  |  | Contact name. |
| `route.from.alias` | string |  |  |  | Nickname/label. |
| `route.from.isSender` | boolean |  |  |  | Sender role. |
| `route.from.isRecipient` | boolean |  |  |  | Recipient role. |
| `route.from.phonePrefix` | string |  |  |  | Dial code, digits only. |
| `route.from.phoneNumber` | string |  |  |  | National number. |
| `route.from.email` | string |  |  |  | Contact email. |
| `route.from.addressLine` | string |  |  |  | Street / address line. |
| `route.from.countryCode` | string |  | ISO-2 |  | Country. |
| `route.from.postalCode` | string |  |  |  | Postal code. |
| `route.from.province` | string |  |  |  | State / province. |
| `route.from.district` | string |  |  |  | County / district. |
| `route.from.city` | string |  |  |  | City. |
| `route.to` | object |  |  |  | Deliver-to address. |
| `route.to.name` | string |  |  |  | Contact name. |
| `route.to.alias` | string |  |  |  | Nickname/label. |
| `route.to.isSender` | boolean |  |  |  | Sender role. |
| `route.to.isRecipient` | boolean |  |  |  | Recipient role. |
| `route.to.phonePrefix` | string |  |  |  | Dial code, digits only. |
| `route.to.phoneNumber` | string |  |  |  | National number. |
| `route.to.email` | string |  |  |  | Contact email. |
| `route.to.addressLine` | string |  |  |  | Street / address line. |
| `route.to.countryCode` | string |  | ISO-2 |  | Country. |
| `route.to.postalCode` | string |  |  |  | Postal code. |
| `route.to.province` | string |  |  |  | State / province. |
| `route.to.district` | string |  |  |  | County / district. |
| `route.to.city` | string |  |  |  | City. |
| `packages` | object |  |  |  | Packages milestone. |
| `packages.items` | array |  |  |  | The parcels. |
| `packages.items[].id` | string |  |  |  | Line id. |
| `packages.items[].type` | string |  | "box", "pallet", "envelope", "tube", "poly_mail", "container", "bulk_cargo" |  | Packaging type. |
| `packages.items[].description` | string |  |  |  | Goods description. |
| `packages.items[].quantity` | string |  |  |  | Piece count (numeric string). |
| `packages.items[].weight` | string |  |  |  | Weight per piece (numeric string). |
| `packages.items[].weightUnit` | string |  | "kg", "lb" |  | Weight unit. |
| `packages.items[].length` | string |  |  |  | Length (numeric string). |
| `packages.items[].width` | string |  |  |  | Width (numeric string). |
| `packages.items[].height` | string |  |  |  | Height (numeric string). |
| `packages.items[].dimUnit` | string |  | "cm", "in" |  | Dimension unit. |
| `packages.items[].value` | string |  |  |  | Declared value (numeric string). |
| `packages.items[].valueCurrency` | string |  | "THB", "USD", "EUR", "GBP" |  | Declared-value currency. |
| `requireDocument` | boolean |  | true, false |  | Package Documents STATE flag — true = documents required, false = "none required" acknowledged, null = not set. NOT the files (those are `attachments`). |
| `service` | object |  |  |  | Service milestone (chosen rate + config). |
| `service.providerId` | string |  |  |  | Chosen carrier id. |
| `service.providerCode` | string |  |  |  | Carrier code. |
| `service.providerName` | string |  |  |  | Carrier display name. |
| `service.price` | object |  |  |  | Frozen quote captured at save. |
| `service.price.thb` | number |  |  |  | Total price, THB. |
| `service.price.zoneName` | string |  |  |  | Rated zone. |
| `service.price.customsThb` | number |  |  |  | Customs component, THB. |
| `service.price.pickupThb` | number |  |  |  | Pickup component, THB (optional). |
| `service.shipDate` | string |  | YYYY-MM-DD |  | Ship date. |
| `service.pickupGroupId` | string |  |  |  | Pickup-option group (null = provider default). |
| `service.pickupGroupName` | string |  |  |  | Pickup group name. |
| `service.pickupAddress` | string |  |  |  | Pickup address. |
| `service.pickupInstructions` | string |  |  |  | Pickup instructions. |
| `service.options` | array |  |  |  | Selected service-option names (string[]). |
| `service.references` | array |  |  |  | Shipment references (string[]). |
| `service.note` | string |  |  |  | Internal note. |
| `notifications` | object |  |  |  | Notifications milestone. |
| `notifications.sender` | object |  |  |  | Sender notification prefs. |
| `notifications.sender.email` | string |  |  |  | Notified email. |
| `notifications.sender.language` | string |  | "en", "th" |  | Email language. |
| `notifications.sender.options` | array |  | "created", "in_transit", "delivered", "exception" |  | Events to notify on (string[]). |
| `notifications.recipients` | array |  |  |  | Recipient notification prefs. |
| `notifications.recipients[].email` | string |  |  |  | Notified email. |
| `notifications.recipients[].language` | string |  | "en", "th" |  | Email language. |
| `notifications.recipients[].options` | array |  | "created", "in_transit", "delivered", "exception" |  | Events to notify on (string[]). |
| `createdAt` | string | ✓ |  |  | ISO-8601 timestamp. |
| `updatedAt` | string | ✓ |  |  | ISO-8601 timestamp. |
| `attachments` | array |  |  |  | Uploaded document FILES (metadata) — embedded on a single-order GET/PUT, omitted when the order has none. Distinct from `requireDocument` (that is the state flag; these are the files). Manage them via the documents sub-resource below. |
| `attachments[].id` | uuid | ✓ |  |  | Document id. |
| `attachments[].name` | string | ✓ |  |  | Display name. |
| `attachments[].description` | string |  |  |  | Optional note. |
| `attachments[].docType` | string | ✓ | "INVOICE", "RECEIPT", "TAX", "VAT", "PACKING_LIST", "CUSTOMS", "OTHER" |  | Document type. |
| `attachments[].contentType` | string |  |  |  | MIME type as uploaded. |
| `attachments[].sizeBytes` | number | ✓ |  |  | Decoded file size in bytes. |
| `attachments[].createdAt` | string | ✓ |  |  | ISO-8601 timestamp. |

**Response example**

```json
{
  "id": "af1326f0-eaf8-4dbd-801e-43d1daf99a23",
  "orderNo": "RDOR202607000039",
  "status": "PROCESSING",
  "fromCountry": "TH",
  "toCountry": "US",
  "serviceProvider": "DHL Express",
  "route": {
    "from": {
      "name": "Green Parker",
      "alias": "Office",
      "isSender": true,
      "isRecipient": false,
      "phonePrefix": "66",
      "phoneNumber": "973331702",
      "email": "green@acme.com",
      "addressLine": "514 Miriam Knolls",
      "countryCode": "TH",
      "postalCode": "10110",
      "province": "Bangkok",
      "district": "Watthana",
      "city": "Bangkok"
    },
    "to": {
      "name": "Martha Howell",
      "alias": "HQ",
      "isSender": false,
      "isRecipient": true,
      "phonePrefix": "1",
      "phoneNumber": "5692748790",
      "email": "martha@acme.com",
      "addressLine": "274 Wellington Road",
      "countryCode": "US",
      "postalCode": "90001",
      "province": "California",
      "district": "Los Angeles County",
      "city": "Los Angeles"
    }
  },
  "packages": {
    "items": [
      {
        "id": "pkg-1",
        "type": "box",
        "description": "Wooden soap",
        "quantity": "1",
        "weight": "1",
        "weightUnit": "kg",
        "length": "42.10",
        "width": "54.80",
        "height": "33.50",
        "dimUnit": "cm",
        "value": "457",
        "valueCurrency": "THB"
      }
    ]
  },
  "requireDocument": true,
  "service": {
    "providerId": "019e984a-f8b4-726f-8f02-0b51e7016f46",
    "providerCode": "RADIMEXP",
    "providerName": "DHL Express",
    "price": {
      "thb": 100,
      "zoneName": "Default",
      "customsThb": 0,
      "pickupThb": 0
    },
    "shipDate": "2026-07-19",
    "pickupGroupId": null,
    "pickupGroupName": null,
    "pickupAddress": "",
    "pickupInstructions": "Ring the bell at the front desk.",
    "options": [],
    "references": [
      "PO-412553",
      "REF-A9BSA"
    ],
    "note": "Handle with care."
  },
  "notifications": {
    "sender": {
      "email": "green@acme.com",
      "language": "en",
      "options": [
        "created",
        "in_transit",
        "delivered",
        "exception"
      ]
    },
    "recipients": [
      {
        "email": "martha@acme.com",
        "language": "en",
        "options": [
          "created",
          "delivered"
        ]
      }
    ]
  },
  "createdAt": "2026-07-17T09:45:27Z",
  "updatedAt": "2026-07-19T18:11:24Z",
  "attachments": [
    {
      "id": "7a1b9c2d-0e3f-4a5b-8c6d-9e0f1a2b3c4d",
      "name": "commercial-invoice.pdf",
      "description": "Signed invoice",
      "docType": "INVOICE",
      "contentType": "application/pdf",
      "sizeBytes": 20480,
      "createdAt": "2026-07-18T04:12:00Z"
    }
  ]
}
```

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 200 | OK | The order. |
| 401 | Unauthorized | The X-API-Key header is missing, invalid, revoked, or expired. |
| 404 | Not Found | No order with that id belongs to your tenant. |
| 503 | Service Unavailable | API-key verification was temporarily unavailable. Retry shortly. |

**Error example**

```json
{
  "ok": false,
  "status": 404,
  "error": "no order found with that id"
}
```

### Update an order

`PUT https://api.parcelradar.io/api/partner/v1/orders/{id}`

Full replacement (PUT): send the complete order — omitted fields are cleared. Status is unchanged (use suspend).

**Request**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `fromCountry` | string | ✓ | ISO-2, in the country list |  | Origin. Must be in the country list; omitted or unknown → 400. |
| `toCountry` | string | ✓ | ISO-2, in the country list |  | Destination. Must be in the country list; omitted or unknown → 400. |
| `serviceProvider` | string | ✓ | ≤ 200 chars |  | Chosen carrier. Omitted → cleared to null. |
| `route` | object | ✓ |  |  | Route milestone object. Omitted → cleared. |
| `route.from` | object |  |  |  | Ship-from address. |
| `route.from.name` | string |  |  |  | Contact name. |
| `route.from.alias` | string |  |  |  | Nickname/label. |
| `route.from.isSender` | boolean |  |  |  | Sender role. |
| `route.from.isRecipient` | boolean |  |  |  | Recipient role. |
| `route.from.phonePrefix` | string |  |  |  | Dial code, digits only. |
| `route.from.phoneNumber` | string |  |  |  | National number. |
| `route.from.email` | string |  |  |  | Contact email. |
| `route.from.addressLine` | string |  |  |  | Street / address line. |
| `route.from.countryCode` | string |  | ISO-2 |  | Country. |
| `route.from.postalCode` | string |  |  |  | Postal code. |
| `route.from.province` | string |  |  |  | State / province. |
| `route.from.district` | string |  |  |  | County / district. |
| `route.from.city` | string |  |  |  | City. |
| `route.to` | object |  |  |  | Deliver-to address. |
| `route.to.name` | string |  |  |  | Contact name. |
| `route.to.alias` | string |  |  |  | Nickname/label. |
| `route.to.isSender` | boolean |  |  |  | Sender role. |
| `route.to.isRecipient` | boolean |  |  |  | Recipient role. |
| `route.to.phonePrefix` | string |  |  |  | Dial code, digits only. |
| `route.to.phoneNumber` | string |  |  |  | National number. |
| `route.to.email` | string |  |  |  | Contact email. |
| `route.to.addressLine` | string |  |  |  | Street / address line. |
| `route.to.countryCode` | string |  | ISO-2 |  | Country. |
| `route.to.postalCode` | string |  |  |  | Postal code. |
| `route.to.province` | string |  |  |  | State / province. |
| `route.to.district` | string |  |  |  | County / district. |
| `route.to.city` | string |  |  |  | City. |
| `packages` | object | ✓ |  |  | Packages milestone object. Omitted → cleared. |
| `packages.items` | array |  |  |  | The parcels. |
| `packages.items[].id` | string |  |  |  | Line id. |
| `packages.items[].type` | string |  | "box", "pallet", "envelope", "tube", "poly_mail", "container", "bulk_cargo" |  | Packaging type. |
| `packages.items[].description` | string |  |  |  | Goods description. |
| `packages.items[].quantity` | string |  |  |  | Piece count (numeric string). |
| `packages.items[].weight` | string |  |  |  | Weight per piece (numeric string). |
| `packages.items[].weightUnit` | string |  | "kg", "lb" |  | Weight unit. |
| `packages.items[].length` | string |  |  |  | Length (numeric string). |
| `packages.items[].width` | string |  |  |  | Width (numeric string). |
| `packages.items[].height` | string |  |  |  | Height (numeric string). |
| `packages.items[].dimUnit` | string |  | "cm", "in" |  | Dimension unit. |
| `packages.items[].value` | string |  |  |  | Declared value (numeric string). |
| `packages.items[].valueCurrency` | string |  | "THB", "USD", "EUR", "GBP" |  | Declared-value currency. |
| `requireDocument` | boolean |  | true, false |  | Package Documents STATE flag (true = required, false = "none required", omitted = cleared). To add files use `attachments` below (or the documents sub-resource). |
| `service` | object | ✓ |  |  | Service milestone object. Omitted → cleared. |
| `service.providerId` | string |  |  |  | Chosen carrier id. |
| `service.providerCode` | string |  |  |  | Carrier code. |
| `service.providerName` | string |  |  |  | Carrier display name. |
| `service.price` | object |  |  |  | Frozen quote captured at save. |
| `service.price.thb` | number |  |  |  | Total price, THB. |
| `service.price.zoneName` | string |  |  |  | Rated zone. |
| `service.price.customsThb` | number |  |  |  | Customs component, THB. |
| `service.price.pickupThb` | number |  |  |  | Pickup component, THB (optional). |
| `service.shipDate` | string |  | YYYY-MM-DD |  | Ship date. |
| `service.pickupGroupId` | string |  |  |  | Pickup-option group (null = provider default). |
| `service.pickupGroupName` | string |  |  |  | Pickup group name. |
| `service.pickupAddress` | string |  |  |  | Pickup address. |
| `service.pickupInstructions` | string |  |  |  | Pickup instructions. |
| `service.options` | array |  |  |  | Selected service-option names (string[]). |
| `service.references` | array |  |  |  | Shipment references (string[]). |
| `service.note` | string |  |  |  | Internal note. |
| `notifications` | object | ✓ |  |  | Notifications milestone object. Omitted → cleared. |
| `notifications.sender` | object |  |  |  | Sender notification prefs. |
| `notifications.sender.email` | string |  |  |  | Notified email. |
| `notifications.sender.language` | string |  | "en", "th" |  | Email language. |
| `notifications.sender.options` | array |  | "created", "in_transit", "delivered", "exception" |  | Events to notify on (string[]). |
| `notifications.recipients` | array |  |  |  | Recipient notification prefs. |
| `notifications.recipients[].email` | string |  |  |  | Notified email. |
| `notifications.recipients[].language` | string |  | "en", "th" |  | Email language. |
| `notifications.recipients[].options` | array |  | "created", "in_transit", "delivered", "exception" |  | Events to notify on (string[]). |
| `attachments` | array |  |  |  | OPTIONAL document files (base64) to ADD in this call — appended, NOT a replacement (existing documents stay; remove them via the sub-resource DELETE). Each entry is an upload body. The response embeds the resulting list. |
| `attachments[].name` | string | ✓ | ≤ 200 chars |  | Display name. |
| `attachments[].description` | string |  | ≤ 500 chars |  | Optional note. |
| `attachments[].docType` | string | ✓ | "INVOICE", "RECEIPT", "TAX", "VAT", "PACKING_LIST", "CUSTOMS", "OTHER" |  | One of the document types. |
| `attachments[].contentBase64` | string | ✓ |  |  | File bytes, base64 (a `data:…;base64,` prefix is accepted). Decoded ≤ 10 MB. |
| `attachments[].contentType` | string |  | ≤ 200 chars |  | MIME type (e.g. application/pdf). |

**Response**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `id` | uuid | ✓ |  |  | Order id. |
| `orderNo` | string | ✓ | e.g. "RDOR202607000042" |  | Reference (RDOR + YYYYMM + 6-digit sequence), unique per tenant. |
| `status` | string | ✓ | "CREATED", "PROCESSING", "IN_TRANSIT", "DELIVERED", "EXCEPTION", "ON_HOLD", "RETURNING", "RETURNED" |  | Lifecycle state. |
| `fromCountry` | string | ✓ | ISO-2, e.g. "TH" |  | Origin, upper-cased. |
| `toCountry` | string | ✓ | ISO-2, e.g. "US" |  | Destination, upper-cased. |
| `serviceProvider` | string |  |  |  | Chosen carrier, else null. |
| `route` | object |  |  |  | Route milestone (ship-from / deliver-to). |
| `route.from` | object |  |  |  | Ship-from address. |
| `route.from.name` | string |  |  |  | Contact name. |
| `route.from.alias` | string |  |  |  | Nickname/label. |
| `route.from.isSender` | boolean |  |  |  | Sender role. |
| `route.from.isRecipient` | boolean |  |  |  | Recipient role. |
| `route.from.phonePrefix` | string |  |  |  | Dial code, digits only. |
| `route.from.phoneNumber` | string |  |  |  | National number. |
| `route.from.email` | string |  |  |  | Contact email. |
| `route.from.addressLine` | string |  |  |  | Street / address line. |
| `route.from.countryCode` | string |  | ISO-2 |  | Country. |
| `route.from.postalCode` | string |  |  |  | Postal code. |
| `route.from.province` | string |  |  |  | State / province. |
| `route.from.district` | string |  |  |  | County / district. |
| `route.from.city` | string |  |  |  | City. |
| `route.to` | object |  |  |  | Deliver-to address. |
| `route.to.name` | string |  |  |  | Contact name. |
| `route.to.alias` | string |  |  |  | Nickname/label. |
| `route.to.isSender` | boolean |  |  |  | Sender role. |
| `route.to.isRecipient` | boolean |  |  |  | Recipient role. |
| `route.to.phonePrefix` | string |  |  |  | Dial code, digits only. |
| `route.to.phoneNumber` | string |  |  |  | National number. |
| `route.to.email` | string |  |  |  | Contact email. |
| `route.to.addressLine` | string |  |  |  | Street / address line. |
| `route.to.countryCode` | string |  | ISO-2 |  | Country. |
| `route.to.postalCode` | string |  |  |  | Postal code. |
| `route.to.province` | string |  |  |  | State / province. |
| `route.to.district` | string |  |  |  | County / district. |
| `route.to.city` | string |  |  |  | City. |
| `packages` | object |  |  |  | Packages milestone. |
| `packages.items` | array |  |  |  | The parcels. |
| `packages.items[].id` | string |  |  |  | Line id. |
| `packages.items[].type` | string |  | "box", "pallet", "envelope", "tube", "poly_mail", "container", "bulk_cargo" |  | Packaging type. |
| `packages.items[].description` | string |  |  |  | Goods description. |
| `packages.items[].quantity` | string |  |  |  | Piece count (numeric string). |
| `packages.items[].weight` | string |  |  |  | Weight per piece (numeric string). |
| `packages.items[].weightUnit` | string |  | "kg", "lb" |  | Weight unit. |
| `packages.items[].length` | string |  |  |  | Length (numeric string). |
| `packages.items[].width` | string |  |  |  | Width (numeric string). |
| `packages.items[].height` | string |  |  |  | Height (numeric string). |
| `packages.items[].dimUnit` | string |  | "cm", "in" |  | Dimension unit. |
| `packages.items[].value` | string |  |  |  | Declared value (numeric string). |
| `packages.items[].valueCurrency` | string |  | "THB", "USD", "EUR", "GBP" |  | Declared-value currency. |
| `requireDocument` | boolean |  | true, false |  | Package Documents STATE flag — true = documents required, false = "none required" acknowledged, null = not set. NOT the files (those are `attachments`). |
| `service` | object |  |  |  | Service milestone (chosen rate + config). |
| `service.providerId` | string |  |  |  | Chosen carrier id. |
| `service.providerCode` | string |  |  |  | Carrier code. |
| `service.providerName` | string |  |  |  | Carrier display name. |
| `service.price` | object |  |  |  | Frozen quote captured at save. |
| `service.price.thb` | number |  |  |  | Total price, THB. |
| `service.price.zoneName` | string |  |  |  | Rated zone. |
| `service.price.customsThb` | number |  |  |  | Customs component, THB. |
| `service.price.pickupThb` | number |  |  |  | Pickup component, THB (optional). |
| `service.shipDate` | string |  | YYYY-MM-DD |  | Ship date. |
| `service.pickupGroupId` | string |  |  |  | Pickup-option group (null = provider default). |
| `service.pickupGroupName` | string |  |  |  | Pickup group name. |
| `service.pickupAddress` | string |  |  |  | Pickup address. |
| `service.pickupInstructions` | string |  |  |  | Pickup instructions. |
| `service.options` | array |  |  |  | Selected service-option names (string[]). |
| `service.references` | array |  |  |  | Shipment references (string[]). |
| `service.note` | string |  |  |  | Internal note. |
| `notifications` | object |  |  |  | Notifications milestone. |
| `notifications.sender` | object |  |  |  | Sender notification prefs. |
| `notifications.sender.email` | string |  |  |  | Notified email. |
| `notifications.sender.language` | string |  | "en", "th" |  | Email language. |
| `notifications.sender.options` | array |  | "created", "in_transit", "delivered", "exception" |  | Events to notify on (string[]). |
| `notifications.recipients` | array |  |  |  | Recipient notification prefs. |
| `notifications.recipients[].email` | string |  |  |  | Notified email. |
| `notifications.recipients[].language` | string |  | "en", "th" |  | Email language. |
| `notifications.recipients[].options` | array |  | "created", "in_transit", "delivered", "exception" |  | Events to notify on (string[]). |
| `createdAt` | string | ✓ |  |  | ISO-8601 timestamp. |
| `updatedAt` | string | ✓ |  |  | ISO-8601 timestamp. |
| `attachments` | array |  |  |  | Uploaded document FILES (metadata) — embedded on a single-order GET/PUT, omitted when the order has none. Distinct from `requireDocument` (that is the state flag; these are the files). Manage them via the documents sub-resource below. |
| `attachments[].id` | uuid | ✓ |  |  | Document id. |
| `attachments[].name` | string | ✓ |  |  | Display name. |
| `attachments[].description` | string |  |  |  | Optional note. |
| `attachments[].docType` | string | ✓ | "INVOICE", "RECEIPT", "TAX", "VAT", "PACKING_LIST", "CUSTOMS", "OTHER" |  | Document type. |
| `attachments[].contentType` | string |  |  |  | MIME type as uploaded. |
| `attachments[].sizeBytes` | number | ✓ |  |  | Decoded file size in bytes. |
| `attachments[].createdAt` | string | ✓ |  |  | ISO-8601 timestamp. |

**Request example**

```json
{
  "fromCountry": "TH",
  "toCountry": "US",
  "serviceProvider": "DHL",
  "route": null,
  "packages": {
    "items": []
  },
  "requireDocument": true,
  "service": null,
  "notifications": null,
  "attachments": [
    {
      "name": "commercial-invoice.pdf",
      "docType": "INVOICE",
      "contentType": "application/pdf",
      "contentBase64": "JVBERi0xLjcKJ..."
    }
  ]
}
```

**Response example**

```json
{
  "id": "3f9a1c62-8d47-4a1e-9b0c-2e5f6a7b8c90",
  "orderNo": "RDOR202607000042",
  "status": "CREATED",
  "fromCountry": "TH",
  "toCountry": "US",
  "serviceProvider": "DHL",
  "route": null,
  "packages": {
    "items": []
  },
  "requireDocument": true,
  "service": null,
  "notifications": null,
  "createdAt": "2026-07-16T09:00:00Z",
  "updatedAt": "2026-07-16T12:05:00Z",
  "attachments": [
    {
      "id": "7a1b9c2d-0e3f-4a5b-8c6d-9e0f1a2b3c4d",
      "name": "commercial-invoice.pdf",
      "description": null,
      "docType": "INVOICE",
      "contentType": "application/pdf",
      "sizeBytes": 20480,
      "createdAt": "2026-07-16T12:05:00Z"
    }
  ]
}
```

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 200 | OK | The order was updated; the body is the stored order. |
| 400 | Bad Request | A country is missing or not a known ISO code, or serviceProvider is over its length limit. |
| 401 | Unauthorized | The X-API-Key header is missing, invalid, revoked, or expired. |
| 404 | Not Found | No order with that id belongs to your tenant. |
| 503 | Service Unavailable | API-key verification was temporarily unavailable. Retry shortly. |

**Error example**

```json
{
  "ok": false,
  "status": 404,
  "error": "no order found with that id"
}
```

### Suspend an order

`POST https://api.parcelradar.io/api/partner/v1/orders/{id}/suspend`

Put an order on hold (→ ON_HOLD). Only from CREATED or PROCESSING.

**Response**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `id` | uuid | ✓ |  |  | Order id. |
| `orderNo` | string | ✓ | e.g. "RDOR202607000042" |  | Reference (RDOR + YYYYMM + 6-digit sequence), unique per tenant. |
| `status` | string | ✓ | "CREATED", "PROCESSING", "IN_TRANSIT", "DELIVERED", "EXCEPTION", "ON_HOLD", "RETURNING", "RETURNED" |  | Lifecycle state. |
| `fromCountry` | string | ✓ | ISO-2, e.g. "TH" |  | Origin, upper-cased. |
| `toCountry` | string | ✓ | ISO-2, e.g. "US" |  | Destination, upper-cased. |
| `serviceProvider` | string |  |  |  | Chosen carrier, else null. |
| `route` | object |  |  |  | Route milestone (ship-from / deliver-to). |
| `route.from` | object |  |  |  | Ship-from address. |
| `route.from.name` | string |  |  |  | Contact name. |
| `route.from.alias` | string |  |  |  | Nickname/label. |
| `route.from.isSender` | boolean |  |  |  | Sender role. |
| `route.from.isRecipient` | boolean |  |  |  | Recipient role. |
| `route.from.phonePrefix` | string |  |  |  | Dial code, digits only. |
| `route.from.phoneNumber` | string |  |  |  | National number. |
| `route.from.email` | string |  |  |  | Contact email. |
| `route.from.addressLine` | string |  |  |  | Street / address line. |
| `route.from.countryCode` | string |  | ISO-2 |  | Country. |
| `route.from.postalCode` | string |  |  |  | Postal code. |
| `route.from.province` | string |  |  |  | State / province. |
| `route.from.district` | string |  |  |  | County / district. |
| `route.from.city` | string |  |  |  | City. |
| `route.to` | object |  |  |  | Deliver-to address. |
| `route.to.name` | string |  |  |  | Contact name. |
| `route.to.alias` | string |  |  |  | Nickname/label. |
| `route.to.isSender` | boolean |  |  |  | Sender role. |
| `route.to.isRecipient` | boolean |  |  |  | Recipient role. |
| `route.to.phonePrefix` | string |  |  |  | Dial code, digits only. |
| `route.to.phoneNumber` | string |  |  |  | National number. |
| `route.to.email` | string |  |  |  | Contact email. |
| `route.to.addressLine` | string |  |  |  | Street / address line. |
| `route.to.countryCode` | string |  | ISO-2 |  | Country. |
| `route.to.postalCode` | string |  |  |  | Postal code. |
| `route.to.province` | string |  |  |  | State / province. |
| `route.to.district` | string |  |  |  | County / district. |
| `route.to.city` | string |  |  |  | City. |
| `packages` | object |  |  |  | Packages milestone. |
| `packages.items` | array |  |  |  | The parcels. |
| `packages.items[].id` | string |  |  |  | Line id. |
| `packages.items[].type` | string |  | "box", "pallet", "envelope", "tube", "poly_mail", "container", "bulk_cargo" |  | Packaging type. |
| `packages.items[].description` | string |  |  |  | Goods description. |
| `packages.items[].quantity` | string |  |  |  | Piece count (numeric string). |
| `packages.items[].weight` | string |  |  |  | Weight per piece (numeric string). |
| `packages.items[].weightUnit` | string |  | "kg", "lb" |  | Weight unit. |
| `packages.items[].length` | string |  |  |  | Length (numeric string). |
| `packages.items[].width` | string |  |  |  | Width (numeric string). |
| `packages.items[].height` | string |  |  |  | Height (numeric string). |
| `packages.items[].dimUnit` | string |  | "cm", "in" |  | Dimension unit. |
| `packages.items[].value` | string |  |  |  | Declared value (numeric string). |
| `packages.items[].valueCurrency` | string |  | "THB", "USD", "EUR", "GBP" |  | Declared-value currency. |
| `requireDocument` | boolean |  | true, false |  | Package Documents STATE flag — true = documents required, false = "none required" acknowledged, null = not set. NOT the files (those are `attachments`). |
| `service` | object |  |  |  | Service milestone (chosen rate + config). |
| `service.providerId` | string |  |  |  | Chosen carrier id. |
| `service.providerCode` | string |  |  |  | Carrier code. |
| `service.providerName` | string |  |  |  | Carrier display name. |
| `service.price` | object |  |  |  | Frozen quote captured at save. |
| `service.price.thb` | number |  |  |  | Total price, THB. |
| `service.price.zoneName` | string |  |  |  | Rated zone. |
| `service.price.customsThb` | number |  |  |  | Customs component, THB. |
| `service.price.pickupThb` | number |  |  |  | Pickup component, THB (optional). |
| `service.shipDate` | string |  | YYYY-MM-DD |  | Ship date. |
| `service.pickupGroupId` | string |  |  |  | Pickup-option group (null = provider default). |
| `service.pickupGroupName` | string |  |  |  | Pickup group name. |
| `service.pickupAddress` | string |  |  |  | Pickup address. |
| `service.pickupInstructions` | string |  |  |  | Pickup instructions. |
| `service.options` | array |  |  |  | Selected service-option names (string[]). |
| `service.references` | array |  |  |  | Shipment references (string[]). |
| `service.note` | string |  |  |  | Internal note. |
| `notifications` | object |  |  |  | Notifications milestone. |
| `notifications.sender` | object |  |  |  | Sender notification prefs. |
| `notifications.sender.email` | string |  |  |  | Notified email. |
| `notifications.sender.language` | string |  | "en", "th" |  | Email language. |
| `notifications.sender.options` | array |  | "created", "in_transit", "delivered", "exception" |  | Events to notify on (string[]). |
| `notifications.recipients` | array |  |  |  | Recipient notification prefs. |
| `notifications.recipients[].email` | string |  |  |  | Notified email. |
| `notifications.recipients[].language` | string |  | "en", "th" |  | Email language. |
| `notifications.recipients[].options` | array |  | "created", "in_transit", "delivered", "exception" |  | Events to notify on (string[]). |
| `createdAt` | string | ✓ |  |  | ISO-8601 timestamp. |
| `updatedAt` | string | ✓ |  |  | ISO-8601 timestamp. |
| `attachments` | array |  |  |  | Uploaded document FILES (metadata) — embedded on a single-order GET/PUT, omitted when the order has none. Distinct from `requireDocument` (that is the state flag; these are the files). Manage them via the documents sub-resource below. |
| `attachments[].id` | uuid | ✓ |  |  | Document id. |
| `attachments[].name` | string | ✓ |  |  | Display name. |
| `attachments[].description` | string |  |  |  | Optional note. |
| `attachments[].docType` | string | ✓ | "INVOICE", "RECEIPT", "TAX", "VAT", "PACKING_LIST", "CUSTOMS", "OTHER" |  | Document type. |
| `attachments[].contentType` | string |  |  |  | MIME type as uploaded. |
| `attachments[].sizeBytes` | number | ✓ |  |  | Decoded file size in bytes. |
| `attachments[].createdAt` | string | ✓ |  |  | ISO-8601 timestamp. |

**Response example**

```json
{
  "id": "3f9a1c62-8d47-4a1e-9b0c-2e5f6a7b8c90",
  "orderNo": "RDOR202607000042",
  "status": "ON_HOLD",
  "fromCountry": "TH",
  "toCountry": "US",
  "serviceProvider": null,
  "route": null,
  "packages": null,
  "requireDocument": null,
  "service": null,
  "notifications": null,
  "createdAt": "2026-07-16T09:00:00Z",
  "updatedAt": "2026-07-16T13:40:00Z"
}
```

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 200 | OK | The order is now ON_HOLD; the body is the stored order. |
| 400 | Bad Request | The order is past the early stage and can't be suspended. |
| 401 | Unauthorized | The X-API-Key header is missing, invalid, revoked, or expired. |
| 404 | Not Found | No order with that id belongs to your tenant. |
| 503 | Service Unavailable | API-key verification was temporarily unavailable. Retry shortly. |

**Error example**

```json
{
  "ok": false,
  "status": 400,
  "error": "order cannot be suspended from its current status"
}
```

## Order Documents

The files attached to an order's Package Documents (invoices, packing lists, …). Bytes travel base64 — no multipart. Also embedded as `attachments` on a single-order GET. Partner lane only.

### List an order's documents

`GET https://api.parcelradar.io/api/partner/v1/orders/{orderId}/documents`

The order's uploaded Package Documents (metadata only, newest first). These are the FILES — distinct from the `documents` milestone (the tab's JSON state). The same list is embedded as `attachments` on a single-order GET.

**Response**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `id` | uuid | ✓ |  |  | Document id. |
| `name` | string | ✓ |  |  | Display name. |
| `description` | string |  |  |  | Optional note. |
| `docType` | string | ✓ | "INVOICE", "RECEIPT", "TAX", "VAT", "PACKING_LIST", "CUSTOMS", "OTHER" |  | Document type. |
| `contentType` | string |  |  |  | MIME type as uploaded. |
| `sizeBytes` | number | ✓ |  |  | Decoded file size in bytes. |
| `createdAt` | string | ✓ |  |  | ISO-8601 timestamp. |

**Response example**

```json
[
  {
    "id": "7a1b9c2d-0e3f-4a5b-8c6d-9e0f1a2b3c4d",
    "name": "commercial-invoice.pdf",
    "description": "Signed invoice",
    "docType": "INVOICE",
    "contentType": "application/pdf",
    "sizeBytes": 20480,
    "createdAt": "2026-07-18T04:12:00Z"
  }
]
```

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 200 | OK | A JSON array of the order's documents (empty [] when none). |
| 401 | Unauthorized | The X-API-Key header is missing, invalid, revoked, or expired. |
| 404 | Not Found | No order with that id belongs to your tenant. |
| 503 | Service Unavailable | API-key verification was temporarily unavailable. Retry shortly. |

**Error example**

```json
{
  "ok": false,
  "status": 404,
  "error": "no order found with that id"
}
```

### Upload an order document

`POST https://api.parcelradar.io/api/partner/v1/orders/{orderId}/documents`

Attach a file to the order. The bytes travel base64-encoded in `contentBase64` (a `data:…;base64,` prefix is accepted) — plain JSON, no multipart. Decoded size ≤ 10 MB.

**Request**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `name` | string | ✓ | ≤ 200 chars |  | Display name. |
| `description` | string |  | ≤ 500 chars |  | Optional note. |
| `docType` | string | ✓ | "INVOICE", "RECEIPT", "TAX", "VAT", "PACKING_LIST", "CUSTOMS", "OTHER" |  | One of the document types. |
| `contentBase64` | string | ✓ |  |  | File bytes, base64 (a `data:…;base64,` prefix is accepted). Decoded ≤ 10 MB. |
| `contentType` | string |  | ≤ 200 chars |  | MIME type (e.g. application/pdf). |

**Response**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `id` | uuid | ✓ |  |  | Document id. |
| `name` | string | ✓ |  |  | Display name. |
| `description` | string |  |  |  | Optional note. |
| `docType` | string | ✓ | "INVOICE", "RECEIPT", "TAX", "VAT", "PACKING_LIST", "CUSTOMS", "OTHER" |  | Document type. |
| `contentType` | string |  |  |  | MIME type as uploaded. |
| `sizeBytes` | number | ✓ |  |  | Decoded file size in bytes. |
| `createdAt` | string | ✓ |  |  | ISO-8601 timestamp. |

**Request example**

```json
{
  "name": "commercial-invoice.pdf",
  "description": "Signed invoice",
  "docType": "INVOICE",
  "contentType": "application/pdf",
  "contentBase64": "JVBERi0xLjcKJ..."
}
```

**Response example**

```json
{
  "id": "7a1b9c2d-0e3f-4a5b-8c6d-9e0f1a2b3c4d",
  "name": "commercial-invoice.pdf",
  "description": "Signed invoice",
  "docType": "INVOICE",
  "contentType": "application/pdf",
  "sizeBytes": 20480,
  "createdAt": "2026-07-18T04:12:00Z"
}
```

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 200 | OK | The document was stored; the body is its metadata. |
| 400 | Bad Request | Missing name/bytes, an unknown docType, invalid base64, or a file over 10 MB. |
| 401 | Unauthorized | The X-API-Key header is missing, invalid, revoked, or expired. |
| 404 | Not Found | No order with that id belongs to your tenant. |
| 503 | Service Unavailable | API-key verification was temporarily unavailable. Retry shortly. |

**Error example**

```json
{
  "ok": false,
  "status": 400,
  "error": "request failed validation",
  "fields": [
    {
      "field": "docType",
      "message": "must be one of INVOICE, RECEIPT, TAX, VAT, PACKING_LIST, CUSTOMS, OTHER"
    }
  ]
}
```

### Update a document

`PUT https://api.parcelradar.io/api/partner/v1/orders/{orderId}/documents/{docId}`

Replace a document's metadata (name / description / docType). Include `contentBase64` to replace the file bytes too; omit it to keep the stored file.

**Request**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `name` | string | ✓ | ≤ 200 chars |  | Display name. |
| `description` | string |  | ≤ 500 chars |  | Optional note. |
| `docType` | string | ✓ | "INVOICE", "RECEIPT", "TAX", "VAT", "PACKING_LIST", "CUSTOMS", "OTHER" |  | One of the document types. |
| `contentBase64` | string |  |  |  | New file bytes (base64) to replace the stored ones; omit/blank to keep them. Decoded ≤ 10 MB. |
| `contentType` | string |  | ≤ 200 chars |  | MIME type of the replacement bytes. |

**Response**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `id` | uuid | ✓ |  |  | Document id. |
| `name` | string | ✓ |  |  | Display name. |
| `description` | string |  |  |  | Optional note. |
| `docType` | string | ✓ | "INVOICE", "RECEIPT", "TAX", "VAT", "PACKING_LIST", "CUSTOMS", "OTHER" |  | Document type. |
| `contentType` | string |  |  |  | MIME type as uploaded. |
| `sizeBytes` | number | ✓ |  |  | Decoded file size in bytes. |
| `createdAt` | string | ✓ |  |  | ISO-8601 timestamp. |

**Request example**

```json
{
  "name": "commercial-invoice-v2.pdf",
  "description": "Re-signed invoice",
  "docType": "INVOICE"
}
```

**Response example**

```json
{
  "id": "7a1b9c2d-0e3f-4a5b-8c6d-9e0f1a2b3c4d",
  "name": "commercial-invoice-v2.pdf",
  "description": "Re-signed invoice",
  "docType": "INVOICE",
  "contentType": "application/pdf",
  "sizeBytes": 20480,
  "createdAt": "2026-07-18T04:12:00Z"
}
```

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 200 | OK | The document was updated; the body is its metadata. |
| 400 | Bad Request | Missing name, an unknown docType, invalid base64, or a replacement file over 10 MB. |
| 401 | Unauthorized | The X-API-Key header is missing, invalid, revoked, or expired. |
| 404 | Not Found | No such document for this order/tenant. |
| 503 | Service Unavailable | API-key verification was temporarily unavailable. Retry shortly. |

**Error example**

```json
{
  "ok": false,
  "status": 404,
  "error": "no document found with that id"
}
```

### Download a document

`GET https://api.parcelradar.io/api/partner/v1/orders/{orderId}/documents/{docId}`

Fetch one document WITH its bytes (base64). Decode `contentBase64` to a Blob/file and save it as `name`.

**Response**

| Field | Type | Required | Values | Default | Note |
| ----- | ---- | :------: | ------ | ------- | ---- |
| `name` | string | ✓ |  |  | Display name (save the file as this). |
| `contentType` | string |  |  |  | MIME type as uploaded. |
| `contentBase64` | string | ✓ |  |  | File bytes, base64 — decode to a Blob/file. |

**Response example**

```json
{
  "name": "commercial-invoice.pdf",
  "contentType": "application/pdf",
  "contentBase64": "JVBERi0xLjcKJ..."
}
```

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 200 | OK | The document with its base64 bytes. |
| 401 | Unauthorized | The X-API-Key header is missing, invalid, revoked, or expired. |
| 404 | Not Found | No such document for this order/tenant. |
| 503 | Service Unavailable | API-key verification was temporarily unavailable. Retry shortly. |

**Error example**

```json
{
  "ok": false,
  "status": 404,
  "error": "no document found with that id"
}
```

### Delete a document

`DELETE https://api.parcelradar.io/api/partner/v1/orders/{orderId}/documents/{docId}`

Remove a document from the order.

**Response codes**

| Code | Status | Description |
| ---- | ------ | ----------- |
| 204 | No Content | The document was deleted; no body. |
| 401 | Unauthorized | The X-API-Key header is missing, invalid, revoked, or expired. |
| 404 | Not Found | No such document for this order/tenant. |
| 503 | Service Unavailable | API-key verification was temporarily unavailable. Retry shortly. |

**Error example**

```json
{
  "ok": false,
  "status": 404,
  "error": "no document found with that id"
}
```