# Fulfilling orders

The OneCart API does what OneCart's Order Processing does, on every channel the app supports, using the same marketplace calls. Base URL: `https://app.getonecart.com/api/v2`. Every endpoint below is also an MCP tool.

## The flow

| Step | What you do | Endpoints |
|---|---|---|
| **1. Orders to ship** | Find orders waiting to ship; mark them picked as you pick | `GET /delivery_orders`, `PUT /orders/{id}/picking`, `PUT /orders/picking` |
| **2. Arrange shipment** | Tell the marketplace (or your courier) how the parcel leaves you; a tracking number is assigned | `GET /orders/{id}/shipping_options` → `POST /orders/{id}/arrange_shipment` (or in bulk), or book your own logistics provider |
| **3. Print** | Print the label and any documents, then hand the parcel over | `POST /orders/print_awbs`, `POST /orders/documents` |

Arrange shipment before you print. Until then the marketplace has no label. OneCart checks this first and returns those orders under `skipped_orders` without calling the marketplace.

## Step 1: orders to ship

`GET /delivery_orders` returns the same queue as Order Processing, across Shopee, Lazada, TikTok, Shopify, WooCommerce, Zalora, Temu and Qoo10. Each row includes the line items to pick.

Optional filters:
- `stage=Check|Print`: the app's tabs.
- `platform`, `shop_id`, `search`, `shipping_provider`.
- `order_nos`: a pasted list, returned in that order.
- `sort=order_date_desc|order_date_asc|ship_by`.

To mark orders picked, as the picking list does in the app, call `PUT /orders/{id}/picking` or `PUT /orders/picking` with `{ "picked": true }`. This only changes OneCart.

## Step 2: arrange shipment

### One endpoint for every marketplace

1. `GET /orders/{id}/shipping_options` returns the `methods` available for the order right now, and the inputs each one needs. For many orders at once, use `GET /orders/shipping_options?order_ids=1,2,3` (up to 50).
2. `POST /orders/{id}/arrange_shipment` with the `method` you chose and its inputs.

| Platform | `method` | Inputs | Same as the app's… |
|---|---|---|---|
| Shopee | `pickup` | `address_id`, `pickup_time_id` | pickup booking |
| Shopee | `dropoff` | none | dropoff |
| Shopee | `non_integrated` (your own courier) | `tracking_number` | own-fleet ship |
| Shopee | per package | `packages: [{package_number, method, address_id, pickup_time_id}]` | pickup per package |
| Lazada | `pack` | none | Pack |
| Lazada | `ready_to_ship` | none (packs first if needed) | Set as Ready to Ship |
| TikTok | `ready_to_ship` | none | Ready to Ship (TikTok's courier) |
| TikTok | `own_tracking` (you deliver) | `tracking_number` or `packages: [{package_id, tracking_number}]`, optional `shipping_provider_id` | seller shipping |
| Zalora | `pack`, then `ready_to_ship` | none | Packed / Ready to Ship |
| Qoo10 | `confirm` | `pickup_date` (YYYYMMDD, today to +7 days) | Confirm with pickup date |
| Temu | `buy_shipping` | `channel_id`, `ship_company_id`, `weight`, `length`, `width`, `height` (+ units), optional `warehouse_id` | Buy Shipping (returns the label and tracking) |
| Temu | `own_tracking` | `package_sn`, `tracking_number`, `ship_company_id` | switch a bought package to your tracking |
| Shopify | `fulfil` | optional `tracking_number`, `logistics_company`, `tracking_url`, `notify_customer` | Confirm |
| WooCommerce | `fulfil` | none | Confirm |

Where to find the inputs:
- **Shopee pickup times** are in `pickup.addresses[].time_slots[]` (`pickup_time_id`, `date`, `time_text`).
- **TikTok shipping providers** and **Temu shipping services** are listed in their `shipping_options`. For Temu, pass the parcel's weight and dimensions as query parameters to get the services.

Before anything is sent, OneCart checks four things, so a bad request never reaches the marketplace:
- The order is yours (`404` otherwise).
- The order isn't already arranged, shipped or cancelled (`422`).
- The method is available now (`METHOD_NOT_AVAILABLE`).
- Your inputs are still valid. It re-reads Shopee slots, TikTok providers and Temu services (`INVALID_PICKUP_SLOT`, `INVALID_SHIPPING_SERVICE`, …).

A marketplace refusal returns `422 PLATFORM_ERROR` with the marketplace's own message. On success, `data.status` is `arranged`, `packed` or `fulfilled`. Then poll `GET /orders/{id}` for `tracking_no` and print.

### Many orders at once

`POST /orders/arrange_shipment` with `{ "orders": [{ "id": 9001, "method": "pickup", "address_id": 111, "pickup_time_id": "..." }, { "id": 9002, "method": "ready_to_ship" }] }` (up to 50).

It returns `202` with a row per order. Rows that fail the checks are marked `failed` straight away, with no marketplace call. Poll `GET /orders/arrange_shipment/{id}` until `status` is `completed`. One order failing never stops the others.

### Split a Shopee order

`POST /orders/{id}/split` with `package_list: [{item_list: [{item_id, model_id, order_item_id, model_quantity}]}]`, then arrange each package with `packages[]`.

### Let OneCart choose

`PUT /orders/{id}` with `mark_as_shipped=true` arranges shipment with OneCart's default choice:
- **Shopee:** pickup at your default address in the 12PM - 3PM slot, or dropoff.
- **Lazada:** pack + ready to ship.
- **TikTok:** ready to ship.
- **Zalora:** pack + ready to ship.
- **Qoo10:** confirm with a pickup date of tomorrow.
- **Shopify / WooCommerce:** fulfil.

If it can't arrange the order, the response warns why and nothing is sent. Use `arrange_shipment` when you need to choose.

### Ship with your own logistics provider (SPX, SingPost)

For Shopify, WooCommerce and Magento orders, the same as "Send to Logistics" in the app:

1. `GET /logistics_providers`: your providers and pickup addresses.
2. `GET /logistics_providers/{id}/pickup_timings`: open pickup dates and slots.
3. `GET /orders/{id}/shippable_items`: the items still to ship. To split an order, assign each parcel its own items.
4. `POST /shipments` with `provider_id`, `delivery_method: "pickup"`, `pickup_address_id`, `pickup_date`, `pickup_time_id` and `parcels: [{order_id, weight, length, width, height, line_items: [{ref, qty}]}]`.
   - The courier is booked straight away.
   - Tracking is then written back automatically: Shopify orders are fulfilled (partially, for split parcels) and WooCommerce orders are updated. Magento orders aren't updated with tracking.
   - An order that already has a live shipment is refused before anything is booked. Cancel the shipment first to re-book.
5. `GET /shipments/{id}/label` prints the courier label. `POST /shipments/{id}/cancel` cancels the shipment (SPX only).

Dropoff isn't available for these providers yet. Use pickup.

### Seller-own-fleet in bulk

`POST /orders/bulk_mark_shipped` runs the app's Bulk Mark Shipped for orders you deliver yourself. Use `action: ready_to_ship`, `attach_tracking` or `delivered`, then poll `GET /orders/bulk_mark_shipped/{id}`.

## Step 3: print

### Labels

`POST /orders/print_awbs` with `{ "order_ids": [...] }` returns labels per shop:

| Platform | What you get back |
|---|---|
| Shopee | `pdf_data_base64` |
| Lazada | `download_urls` (`with_sku_list` adds a picking list) |
| TikTok | `download_urls` per package (`document_type` can add the packing slip) |
| Zalora | `status: "processing"` and a `poll` URL |
| Qoo10 | the Qxpress waybill |

Orders that can't have a label yet come back in `skipped_orders`:

| `error_code` | What to do |
|---|---|
| `SHIPMENT_NOT_ARRANGED` | Arrange shipment, then print once there's a tracking number |
| `ORDER_NOT_PACKED` | Pack (Lazada) |
| `ORDER_UNPAID` / `ORDER_ON_HOLD` | Wait |
| `ORDER_CANCELLED` | Don't ship |
| `RECENTLY_PRINTED` | Printed in the last 30 seconds |

### Other documents

`POST /orders/documents` with `{ "order_ids": [...], "document": "..." }` prints the same documents as the app:

| `document` | What it is |
|---|---|
| `lazada_invoice`, `lazada_carrier_manifest`, `lazada_label_and_invoice` | Lazada documents. Lazada orders still `pending` are packed first, as the app does, and listed in `packed_orders`. |
| `shopee_label_and_receipt` | Shopee label + receipt |
| `tiktok_label_and_packing_slip` | TikTok label + packing slip |
| `zalora_label`, `zalora_invoice`, `zalora_shipping_docs` | Zalora documents |
| `qoo10_waybill` | Qxpress waybill |
| `order_receipt`, `order_pdf`, `custom_awb` | Receipt, order page PDF, OneCart's own A6 label |
| `picking_list` | One row per SKU with quantity, bin location and orders. `output=json` returns the data instead of a PDF. Printing it marks the orders picked, as in the app. |

Documents that take longer return `status: "processing"` and a `poll` URL (`GET /orders/documents/{job_id}`).

## Order statuses and what to do next

| Stage | Shopee | Lazada | TikTok | Zalora | Temu | Next step |
|---|---|---|---|---|---|---|
| Not paid / on hold | `UNPAID` | `unpaid` | `UNPAID`, `ON_HOLD` |  | `PENDING` | Wait |
| Ready to arrange | `READY_TO_SHIP`, `RETRY_SHIP` | `pending`, `packed` | `AWAITING_SHIPMENT` | `pending`, `packed` | `UN_SHIPPING` | Arrange shipment |
| Arranged, label available | `PROCESSED` | `ready_to_ship` | `AWAITING_COLLECTION` | `ready_to_ship` | `SHIPPED` (label bought) | Print, hand over |
| With the courier | `SHIPPED` | `shipped` | `IN_TRANSIT` | `shipped` |  | Nothing |
| Done | `TO_CONFIRM_RECEIVE`, `COMPLETED` | `delivered` | `DELIVERED`, `COMPLETED` | `delivered` | `RECEIPTED` | Nothing |
| Cancelled / returned | `IN_CANCEL`, `CANCELLED`, `TO_RETURN` | `canceled`, `failed`, `returned` | `CANCELLED` | `canceled`, `returned` | `CANCELED` | Don't ship |

## A typical integration

1. **Poll for orders.** Every few minutes, call `GET /delivery_orders?stage=Check`.
2. **Get the options.** Call `GET /orders/shipping_options?order_ids=...` and choose a method per order.
3. **Arrange shipment.** Call `POST /orders/arrange_shipment` with your choices, and poll it.
4. **Print.** On a later poll, call `POST /orders/print_awbs` (and `/orders/documents` if needed) for the orders that now have a `tracking_no`.
5. **Retry skipped orders.** Leave anything in `skipped_orders` for the next poll.
