# Quote and order in one flow

This playbook walks through the Uniorder API (`/api/v1/uniorder/...`) request by request, in the order an integration is built: authenticate, quote, create at the chosen `rate_id`, print the label, read, track and cancel the order, and process shipments in batches. One quote lists every way the account can ship a parcel: delivery by the business itself and, on request, every label carrier service. Ordering with a `rate_id` creates the order for that service: a delivery order, or a label order with the label bought at the carrier service quoted. It is written for developers of online stores, order-management systems and ERPs that ship through a business account.

## 1. What you can build

The examples below follow one business: **Fleurs du Plateau**, a florist at 4500 Rue Saint-Denis, Montreal (H2J 2L3), that sells bouquets online. A typical parcel is one 1.2 kg box of 40 × 25 × 25 cm, going to Jane Recipient at 6841 Rue Saint-Denis, Montreal (H2S 2S3), under the web order `WEB-10045`.

- **A checkout that offers every shipping option.** The store quotes the parcel once and shows same-day local delivery beside every carrier label service of the account, each with its price, then creates the order at the option the customer chose.
- **Automatic label printing.** When the order is created, the store downloads the label PDF and sends it to the packing-station printer, whether the parcel is delivered by the business or by a carrier.
- **An order page with live tracking.** The customer's order page shows the status and the event timeline of the shipment, with the proof of delivery once the bouquet is delivered.
- **A nightly batch from the ERP.** The day's wholesale orders are quoted and created in one queued job of up to 500 rows, and each result is matched back to its order line by `reference`.

## 2. What this playbook covers

This is the step-by-step playbook of the Uniorder API. The overview of what Uniorder offers, and why, is in **Uniorder: one API for every shipment**; this playbook gives the requests, responses and checks for each call.

Uniorder is the recommended single entry point for new integrations that ship parcels by local delivery or by carrier label: it replaces separate calls to the local delivery API and the carrier label API with one request shape. The earlier endpoints described in **Own-fleet pickup and delivery** and **Carrier labels** remain available and unchanged. Uniorder does not apply to shipping services booked by a customer account or to storage and ship-out orders; use **Shipping services** and **Storage and ship-out** for those.

## 3. Before you start

- **Account.** Use a business (client) account, or an employee account of the business, with API permission. A customer account of the business can also call Uniorder and is always quoted and billed as itself. Creating a delivery order requires the place-order permission.
- **Customers.** A client or employee account can quote and order for one of its customers with `customer_id` or `customer_code` in the quote; the `rate_id` then carries that customer, and the price follows the customer's plan.
- **Label services.** To receive `label_service` rates, the account (or the named customer) needs at least one label carrier account configured.
- **Test data.** Use an address inside the delivery area of the business for `self_delivery` rates, and test references such as `WEB-10045` that can be cancelled afterwards.
- **Tokens.** Request the access token from your server and keep it there. Never send it to a browser or a mobile app.
- **Placeholders.** Replace `YOUR_HOST` with the API host of your environment and `ACCESS_TOKEN` with the token from step 4.

## 4. Authenticate

Every Uniorder call is made on behalf of an account. Log in once from your server, store the returned token and send it on every request.

**REST:** `POST /api/v1/user/login` — [REST handbook](/api/documentation#/paths/v1-user-login/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/user/login \
  -H "Content-Type: application/json" \
  -d '{"email":"orders@fleursduplateau.example","password":"your_password"}'
```

```json
{
  "result": true,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
}
```

- `access_token`: place it in the header of every later request:

```
Authorization: Bearer ACCESS_TOKEN
```

GraphQL uses the same header on `POST /api/graphql`.

**GraphQL:** `userLogin` ([GraphQL handbook](/api/graphql/documentation#/user/userLogin))

**Verification:** login returns `access_token`. Subsequent requests without this token return `401`.

## 5. Quote every service

The quote lists every way the parcel can be shipped, with a price and a `rate_id` for each. The checkout shows the rates as options; nothing is created or booked.

**REST:** `POST /api/v1/uniorder/rate` — [REST handbook](/api/documentation#/paths/v1-uniorder-rate/post)

The sender and the recipient are complete addresses; only `from_address_2` and `to_address_2` are optional. Each package needs `weight`, `length`, `width` and `height`. Set `quote_labels` to `true` to add the label carrier services; the names and telephones of both ends are then required. A client or employee account may quote for one of its customers with `customer_id` or `customer_code`. A delivery window (`time_window_start`, `time_window_end`, format `YYYY-MM-DD HH:MM:SS`) is taken into account when the price depends on it.

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/rate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "D",
    "from_name": "Fleurs du Plateau",
    "from_telephone": "5145550100",
    "from_address": "4500 Rue Saint-Denis",
    "from_city": "Montreal",
    "from_province": "QC",
    "from_country": "CA",
    "from_postcode": "H2J2L3",
    "to_name": "Jane Recipient",
    "to_telephone": "5145550199",
    "to_address": "6841 Rue Saint-Denis",
    "to_address_2": "Apt 2",
    "to_city": "Montreal",
    "to_province": "QC",
    "to_country": "CA",
    "to_postcode": "H2S2S3",
    "time_window_start": "2026-10-02 13:00:00",
    "time_window_end": "2026-10-02 17:00:00",
    "quote_labels": true,
    "packages": [{
      "weight": 1.2,
      "weight_unit": 2,
      "length": 40,
      "width": 25,
      "height": 25,
      "dimension_unit": 2
    }]
  }'
```

`weight_unit`: `1` g, `2` kg, `3` oz, `4` lb. `dimension_unit`: `1` mm, `2` cm, `3` m, `4` in.

```json
{
  "result": true,
  "rates": [
    {
      "rate_id": "eyJpdiI6Ik1rT2Z...",
      "type": "self_delivery",
      "channel_id": null,
      "shipping_price": "14.60",
      "currency": "CAD",
      "price_details": { "shipping_fee": 12.92, "sub_total": "12.92" },
      "warning": null
    },
    {
      "rate_id": "eyJpdiI6IlpxR0...",
      "type": "label_service",
      "shipping_price": "18.40",
      "currency": "CAD",
      "shipping_method_id": 72,
      "shipping_method_name": "UPS",
      "carrier_name": "ups",
      "service_code": "ups_standard",
      "service_name": "UPS STANDARD",
      "transit_days": 3
    }
  ],
  "errors": []
}
```

- `type` `self_delivery`: delivery by the business. At most one per quote.
- `type` `label_service`: one per service of each label account. Show `service_name`, `shipping_price` and `transit_days` to the customer.
- `errors` lists what could not be quoted, with its `type`. An address outside the delivery area is an error of type `self_delivery` with code `OUT_OF_DELIVERY_AREA`; show the label services only.
- `rate_id` is valid for 30 minutes and only for the account that asked for the quote. Keep it with the checkout session.
- `result` is `true` when at least one rate was found.

**GraphQL:** `uniorderRate` ([GraphQL handbook](/api/graphql/documentation#/orders/uniorderRate)). The answer is a JSON scalar, so the operation has no selection set.

```graphql
mutation QuoteBouquet($packages: [Json]!) {
  uniorderRate(
    type: "D"
    from_name: "Fleurs du Plateau"
    from_telephone: "5145550100"
    from_address: "4500 Rue Saint-Denis"
    from_city: "Montreal"
    from_province: "QC"
    from_country: "CA"
    from_postcode: "H2J2L3"
    to_name: "Jane Recipient"
    to_telephone: "5145550199"
    to_address: "6841 Rue Saint-Denis"
    to_city: "Montreal"
    to_province: "QC"
    to_country: "CA"
    to_postcode: "H2S2S3"
    quote_labels: true
    packages: $packages
  )
}
```

Variables:

```json
{ "packages": [{ "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }] }
```

**Verification:** `rates` contains a `self_delivery` rate for an in-area address and, with `quote_labels`, one `label_service` rate per carrier service. Nothing is created.

## 6. Create the order at the chosen rate

When the customer pays, the store creates the order with the `rate_id` of the option chosen and the same shipment. The `rate_id` decides the service; nothing else in the request selects it.

**REST:** `POST /api/v1/uniorder` — [REST handbook](/api/documentation#/paths/v1-uniorder/post)

Send an `Idempotency-Key` header, unique per order, on every create. A retry with the same key and the same body returns the first answer with `replayed` `true` and does not create a second order.

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: checkout-10045" \
  -d '{
    "rate_id": "eyJpdiI6Ik1rT2Z...",
    "ref": "WEB-10045",
    "type": "D",
    "from_name": "Fleurs du Plateau",
    "from_telephone": "5145550100",
    "from_address": "4500 Rue Saint-Denis",
    "from_city": "Montreal",
    "from_province": "QC",
    "from_country": "CA",
    "from_postcode": "H2J2L3",
    "to_name": "Jane Recipient",
    "to_telephone": "5145550199",
    "to_email": "jane@example.com",
    "to_address": "6841 Rue Saint-Denis",
    "to_address_2": "Apt 2",
    "to_city": "Montreal",
    "to_province": "QC",
    "to_country": "CA",
    "to_postcode": "H2S2S3",
    "time_window_start": "2026-10-02 13:00:00",
    "time_window_end": "2026-10-02 17:00:00",
    "delivery_instruction": "Ring the bell at the side door.",
    "packages": [{
      "ref": "WEB-10045-1",
      "weight": 1.2,
      "weight_unit": 2,
      "length": 40,
      "width": 25,
      "height": 25,
      "dimension_unit": 2
    }]
  }'
```

A `self_delivery` rate creates a delivery order. For `type` `D` the recipient is the stop; set `need_pick_up` to `1` to have the parcel collected at the sender. For `type` `P` the sender is the stop.

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "ref": "WEB-10045",
  "tracking_numbers": ["SR26092800001"],
  "shipping_price": "14.60",
  "quoted_price": "14.60",
  "currency": "CAD"
}
```

A `label_service` rate creates a label order and buys the label at the carrier service quoted. `type` must be `D`, and `from_name`, `from_telephone`, `to_name` and `to_telephone` are required. Had the customer chosen UPS STANDARD, the answer would be:

```json
{
  "result": true,
  "type": "label_service",
  "id": 123457,
  "ref": "WEB-10045",
  "tracking_numbers": ["SR26092800002"],
  "shipping_price": "18.40",
  "quoted_price": "18.40",
  "currency": "CAD",
  "label": {
    "carrier_name": "ups",
    "service_code": "ups_standard",
    "main_tracking_number": "1Z999AA10123456784",
    "label_status": "ready",
    "shipping_label": "JVBERi0xLjQK..."
  }
}
```

- `id`: store it with the web order; every later call uses it.
- `tracking_numbers`: the shipment's own tracking numbers, one per package.
- `shipping_price`: the price charged. The order is priced when it is created; `quoted_price` is the price in the quote. The two can differ.
- `label.main_tracking_number` and `label.shipping_label` (label order only): the carrier's tracking number and the label PDF in base64.
- `result` `false` with code `LABEL_PURCHASE_FAILED` (label order only): the order exists but has no label. Keep the `id` and continue with step 11.

**GraphQL:** `uniorderCreate` ([GraphQL handbook](/api/graphql/documentation#/orders/uniorderCreate))

```graphql
mutation CreateBouquetOrder($packages: [Json]!) {
  uniorderCreate(
    rate_id: "eyJpdiI6Ik1rT2Z..."
    ref: "WEB-10045"
    type: "D"
    from_name: "Fleurs du Plateau"
    from_telephone: "5145550100"
    from_address: "4500 Rue Saint-Denis"
    from_city: "Montreal"
    from_province: "QC"
    from_country: "CA"
    from_postcode: "H2J2L3"
    to_name: "Jane Recipient"
    to_telephone: "5145550199"
    to_address: "6841 Rue Saint-Denis"
    to_city: "Montreal"
    to_province: "QC"
    to_country: "CA"
    to_postcode: "H2S2S3"
    packages: $packages
  )
}
```

Variables:

```json
{ "packages": [{ "ref": "WEB-10045-1", "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }] }
```

**Verification:** `result` is `true` and `id` is set. A `rate_id` that has expired or belongs to another account returns `400` with code `RATE_ID_INVALID`, and nothing is created.

## 7. Print the label

The packing station prints the label as soon as the order exists. The same call returns the business's own label for a delivery order and the carrier label bought for a label order.

**REST:** `GET /api/v1/uniorder/{orderId}/label` — [REST handbook](/api/documentation#/paths/v1-uniorder-orderId--label/get)

```bash
curl "https://YOUR_HOST/api/v1/uniorder/123456/label?hide_sender_address=0" \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "format": "pdf",
  "tracking_numbers": ["SR26092800001"],
  "pdf_data": "JVBERi0xLjQK..."
}
```

- `pdf_data`: the label PDF in base64. Decode it and send the file to the printer.
- `hide_sender_address`, `hide_receiver_address` (`1` to hide): apply to the business's own label of a delivery order.
- `label_status` (label order): `ready` when the file is returned. When the carrier has not produced the file yet, the answer is `200` with `result` `false` and `label_status` `pending`; request the label again later.
- This call never buys a label: a label that has not been bought returns `409` with code `LABEL_PURCHASE_FAILED`. Buy it with step 11.

**GraphQL:** `uniorderLabel` ([GraphQL handbook](/api/graphql/documentation#/orders/uniorderLabel))

```graphql
query {
  uniorderLabel(order_id: 123456, hide_sender_address: 0)
}
```

**Verification:** `result` is `true` and the decoded `pdf_data` opens as a PDF showing the order's tracking number.

## 8. Read the order

The store reads the order to show its status, addresses and packages on the order page or in a customer-service screen.

**REST:** `GET /api/v1/uniorder/{orderId}` — [REST handbook](/api/documentation#/paths/v1-uniorder-orderId/get)

```bash
curl https://YOUR_HOST/api/v1/uniorder/123456 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "ref": "WEB-10045",
  "order_type": "D",
  "status": "pending",
  "created_at": "2026-10-02 09:14:05",
  "time_window_start": "2026-10-02 13:00:00",
  "time_window_end": "2026-10-02 17:00:00",
  "from": { "name": "Fleurs du Plateau", "address": "4500 Rue Saint-Denis", "city": "Montreal", "postcode": "H2J2L3" },
  "to": { "name": "Jane Recipient", "address": "6841 Rue Saint-Denis", "address_2": "Apt 2", "city": "Montreal", "postcode": "H2S2S3" },
  "packages": [
    { "id": 998877, "ref": "WEB-10045-1", "tracking_number": "SR26092800001", "weight": 1.2 }
  ],
  "shipping_price": "14.60",
  "currency": "CAD"
}
```

- `type`: `self_delivery` or `label_service`; the other fields follow the same shape for both.
- `status`: `pending`, `in_transit`, `out_for_pickup`, `out_for_delivery`, `ready_for_self_pickup`, `delivered`, `exception` or `cancelled` for a delivery order, and `label_pending`, `label_purchased` or `cancelled` for a label order.
- `label` (label order only): the carrier, the service, `carrier_tracking_numbers` and `label_status` (`not_purchased`, `pending`, `ready` or `failed`).

**GraphQL:** `uniorder` ([GraphQL handbook](/api/graphql/documentation#/orders/uniorder))

```graphql
query {
  uniorder(order_id: 123456)
}
```

**Verification:** the order returns its `status` and `packages`, and `ref` matches the web order.

## 9. Track the order

The order page shows the shipment's timeline. Read it when the customer opens the page, or keep it current from webhooks.

**REST:** `GET /api/v1/uniorder/{orderId}/tracking` — [REST handbook](/api/documentation#/paths/v1-uniorder-orderId--tracking/get)

```bash
curl https://YOUR_HOST/api/v1/uniorder/123456/tracking \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "status": "delivered",
  "tracking_numbers": ["SR26092800001"],
  "events": [
    { "code": "delivered", "description": "Delivered", "location": "Montreal", "time": "2026-10-02 15:42:10", "time_zone": "America/Toronto", "source": "shipper" }
  ],
  "proofs": [
    { "type": "photo", "url": "https://YOUR_HOST/storage/pod/123456.jpg", "uploaded_at": "2026-10-02 15:42:08" }
  ]
}
```

- `events`: the timeline, newest first, each with `code`, `description`, `location` and time.
- `proofs`: proof-of-delivery files. Show them once `status` is `delivered`.
- `carrier` (label order only): the carrier's name, tracking number and tracking link (`tracking_url`).

**GraphQL:** `uniorderTracking` ([GraphQL handbook](/api/graphql/documentation#/orders/uniorderTracking))

```graphql
query {
  uniorderTracking(order_id: 123456)
}
```

**Verification:** the tracking call returns `result` `true`, the order `status` and its `events`.

## 10. Cancel the order

When the customer cancels the web order, the store cancels the shipment with the same call for a delivery order and a label order. A label is voided with its carrier first.

**REST:** `POST /api/v1/uniorder/{orderId}/cancel` — [REST handbook](/api/documentation#/paths/v1-uniorder-orderId--cancel/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/123456/cancel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cancel-10045" \
  -d '{}'
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "already_cancelled": false,
  "message": "The order has been cancelled."
}
```

- `already_cancelled`: `true` when the order was cancelled before this call. Treat it as success.
- When the order is not cancelled, the answer is `409` and the order is unchanged: `ORDER_STATUS_NOT_CANCELLABLE` (too late to cancel), `ORDER_CANCEL_REFUSED` (cannot be cancelled now) or `LABEL_CANCEL_FAILED` (the carrier did not void the label). Keep the web order open and handle the shipment manually.

**GraphQL:** `uniorderCancel` ([GraphQL handbook](/api/graphql/documentation#/orders/uniorderCancel))

```graphql
mutation {
  uniorderCancel(order_id: 123456)
}
```

**Verification:** `result` is `true`. Cancelling the same order again returns `already_cancelled` `true`.

## 11. Buy a label later (only after LABEL_PURCHASE_FAILED)

This step applies only to a label order whose create answered `LABEL_PURCHASE_FAILED`. The answer was `200` with `result` `false`, the code `LABEL_PURCHASE_FAILED` and the order `id`: the order is kept without a label. Do not submit the order again; buy the label for that order.

**REST:** `POST /api/v1/uniorder/{orderId}/label` — [REST handbook](/api/documentation#/paths/v1-uniorder-orderId--label/post)

The create that failed to buy the label answered:

```json
{
  "result": false,
  "code": "LABEL_PURCHASE_FAILED",
  "type": "label_service",
  "id": 123458,
  "ref": "WEB-10046",
  "tracking_numbers": ["SR26092800003"],
  "quoted_price": "18.40",
  "message": "The quoted service is not offered for this shipment."
}
```

Buy the label for order `123458`:

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/123458/label \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: label-10046" \
  -d '{}'
```

The label is bought at the service chosen when the order was created. To buy at another service of the same account, send a new `label_service` `rate_id` from step 5 in the body (`{"rate_id": "eyJpdiI6IlpxR0..."}`). A label that is already bought is returned and is not bought again.

```json
{
  "result": true,
  "type": "label_service",
  "id": 123458,
  "ref": "WEB-10046",
  "shipping_price": "18.40",
  "label": {
    "carrier_name": "ups",
    "service_code": "ups_standard",
    "main_tracking_number": "1Z999AA10123456791",
    "label_status": "ready",
    "shipping_label": "JVBERi0xLjQK..."
  }
}
```

- `label.shipping_label`: the label PDF in base64; print it as in step 7.
- `result` `false` with `LABEL_PURCHASE_FAILED` again: the carrier still refused. Retry later or buy at another service with a new `rate_id`.

**GraphQL:** `uniorderPurchaseLabel` ([GraphQL handbook](/api/graphql/documentation#/orders/uniorderPurchaseLabel))

```graphql
mutation {
  uniorderPurchaseLabel(order_id: 123458)
}
```

**Verification:** `result` is `true` and `label.shipping_label` carries the PDF, or `label.label_status` is `pending` while the carrier produces the file.

## 12. Batches

Batches quote or create many shipments in one call, for example the ERP's wholesale orders. Every row goes through the single call and returns what that call would return; a row that fails does not stop the other rows.

**REST:** `POST /api/v1/uniorder/rate/batch` — [REST handbook](/api/documentation#/paths/v1-uniorder-rate-batch/post) · `POST /api/v1/uniorder/batch` — [REST handbook](/api/documentation#/paths/v1-uniorder-batch/post)

Up to 20 rows per call, answered in the same response: `shipments` for the quote batch, `orders` for the create batch. Each row has the same fields as the single call, plus an optional `reference` that is returned with its result.

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/batch \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: erp-2026-10-01" \
  -d '{
    "orders": [
      {
        "reference": "ERP-7781",
        "rate_id": "eyJpdiI6Ik1rT2Z...",
        "ref": "ERP-7781",
        "from_name": "Fleurs du Plateau",
        "from_telephone": "5145550100",
        "from_address": "4500 Rue Saint-Denis",
        "from_city": "Montreal",
        "from_province": "QC",
        "from_country": "CA",
        "from_postcode": "H2J2L3",
        "to_name": "Jane Recipient",
        "to_telephone": "5145550199",
        "to_address": "6841 Rue Saint-Denis",
        "to_city": "Montreal",
        "to_province": "QC",
        "to_country": "CA",
        "to_postcode": "H2S2S3",
        "packages": [{ "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }]
      }
    ]
  }'
```

```json
{
  "result": true,
  "count": 1,
  "results": [
    { "index": 0, "reference": "ERP-7781", "status": 200, "body": { "result": true, "type": "self_delivery", "id": 123460 } }
  ]
}
```

- `results`: one per row, with the row's `index`, its `reference`, and the `status` and `body` the single call would return. Match each result to its order line by `reference`.

**REST:** `POST /api/v1/uniorder/rate/batch-async` — [REST handbook](/api/documentation#/paths/v1-uniorder-rate-batch-async/post) · `POST /api/v1/uniorder/batch-async` — [REST handbook](/api/documentation#/paths/v1-uniorder-batch-async/post) · `GET /api/v1/uniorder/jobs/{jobId}` — [REST handbook](/api/documentation#/paths/v1-uniorder-jobs-jobId/get)

Up to 500 rows, queued as one job. The call returns a `job_id`; read the job until `status` is `done`, then read `results`. The same batch sent again while the first is still queued returns the first job with `duplicate` `true`.

```bash
curl https://YOUR_HOST/api/v1/uniorder/jobs/8813 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "job_id": 8813,
  "kind": "create",
  "status": "done",
  "count": 1,
  "results": [
    { "index": 0, "reference": "ERP-7781", "status": 200, "body": { "result": true, "type": "self_delivery", "id": 123460 } }
  ]
}
```

- `status`: `queued`, `done`, or `failed` with a `message` when the job could not be processed.
- A job runs once and is not retried. A `rate_id` that expires before its row runs returns `RATE_ID_INVALID` for that row; send the create job soon after the quote job is done.

**GraphQL:** `uniorderRateBatch` ([GraphQL handbook](/api/graphql/documentation#/orders/uniorderRateBatch)) · `uniorderCreateBatch` ([GraphQL handbook](/api/graphql/documentation#/orders/uniorderCreateBatch)) · `uniorderRateBatchAsync` ([GraphQL handbook](/api/graphql/documentation#/orders/uniorderRateBatchAsync)) · `uniorderCreateBatchAsync` ([GraphQL handbook](/api/graphql/documentation#/orders/uniorderCreateBatchAsync)) · `uniorderJob` ([GraphQL handbook](/api/graphql/documentation#/orders/uniorderJob))

```graphql
query {
  uniorderJob(job_id: 8813)
}
```

**Verification:** a batch returns one result per row; an async job reaches `status` `done`.

## 13. Handling errors

| Situation | HTTP status | Code | What the integration does |
|---|---|---|---|
| A required field is missing or malformed | 400 | `VALIDATION_FAILED` | Correct the field named in `message` and send the request again. |
| The recipient is outside the delivery area (quote) | 200 | `OUT_OF_DELIVERY_AREA` in `errors` | Offer only the `label_service` rates. |
| The `rate_id` has expired, is malformed or belongs to another account | 400 | `RATE_ID_INVALID` | Request a new quote and create with its `rate_id`. Nothing was created. |
| The label order was created but its label was not bought | 200 (`result` `false`) | `LABEL_PURCHASE_FAILED` | Keep the `id`; buy the label with `POST /api/v1/uniorder/{orderId}/label`. Never create the order again. |
| The label is requested before it was bought | 409 | `LABEL_PURCHASE_FAILED` | Buy the label with `POST /api/v1/uniorder/{orderId}/label`. |
| The order is too far along to cancel | 409 | `ORDER_STATUS_NOT_CANCELLABLE` | Leave the order as it is; handle the return separately. |
| The order cannot be cancelled now | 409 | `ORDER_CANCEL_REFUSED` | Leave the order as it is; retry later or contact the business. |
| The carrier did not void the label | 409 | `LABEL_CANCEL_FAILED` | The order is unchanged; retry the cancel later. |
| The order or job does not exist or belongs to another account | 404 | `ORDER_NOT_FOUND` | Check the `id` stored with the web order. |
| An `Idempotency-Key` is reused with a different body | 409 | `IDEMPOTENCY_CONFLICT` | Use a new key for a different request. |
| The token is missing or expired, or the account may not place orders | 401 | — | Log in again; check the account's permissions. |

## Test checklist

Use a test `ref` such as `WEB-10045`:

- [ ] The quote returns a `self_delivery` rate for an in-area address.
- [ ] With `quote_labels`, the quote returns `label_service` rates, each with a `rate_id`.
- [ ] Ordering with a `self_delivery` `rate_id` returns `id` and `tracking_numbers`.
- [ ] Ordering with a `label_service` `rate_id` returns the label of the service quoted.
- [ ] The same `Idempotency-Key` does not create a second order.
- [ ] A `rate_id` older than 30 minutes returns `RATE_ID_INVALID`.
- [ ] The label of each order decodes to a printable PDF.
- [ ] The order, its label and its tracking can be read with the `id` from create.
- [ ] Cancelling a test order returns `result: true`; cancelling it again returns `already_cancelled: true`.
- [ ] After `LABEL_PURCHASE_FAILED`, `POST /api/v1/uniorder/{orderId}/label` buys the label for the same order.
- [ ] A batch of two rows returns two results with their `reference`.
