API reference

API v2 — items

The v2 surface cleans up the box vocabulary: every box of an order is an item, addressed only by your own references. Everything else — authentication, scopes, errors, the polling model — is unchanged from v1.

v1 and v2 are separate, parallel contracts. v1 keeps serving exactly as documented on the other pages of this portal; v2 is where new vocabulary lands. Pick one version per integration and migrate deliberately — never mix versions inside one workflow.

What changes on v2, completely:

  • The box array on create is items[] (v1: parcels[]), and your per-box reference is item_id (v1: item_id).
  • An item has no Starmile tracking number of its own. v1's items[].parcel_id is gone — you address a item by your item_id or its merchant_tracking, and Starmile's only tracking reference is the order's tracking_number.
  • The create response returns the Starmile reference as tracking_number and echoes your own order_id back (v1 overloaded order_id with the Starmile number).
  • Status-pool rows name your order reference order_id (v1: external_parent_id) and the item reference item_id (v1: external_id). The status vocabulary itself — every status string, including the customs ones — is identical to v1.

Endpoints that only read — GET /api/v2/services, GET /api/v2/rates, GET /api/v2/orders/label — and POST /api/v2/partner/events behave exactly as their v1 twins; only the path is versioned.

Migrating the status pool: the cursor space is new

The v2 pool's cursorcomes from a different id space than v1's. A cursor you stored while polling v1 is meaningless on v2 — never carry it over. When you switch, drain v2 from since=0 (optionally per order via external_parent_id) and de-duplicate against what you already processed on v1, then continue with v2 cursors only.

Create an order (v2)

POST/api/v2/orders

Identical behavior to v1 — same required fields, same service_id resolution, idempotent on your order_id, same region handling — with the body's parcels[] spelled items[] and item_id spelled item_id. All other fields (recipient, delivery, products, gov_id, shipping_cost, …) are unchanged from Create an order.

A replayed order_id answers 200 with duplicate: true and the same body shape.

Body — what differs from v1

itemsarray, requiredoptional
One entry per physical box (v1: parcels[]). Same per-box fields: merchant_tracking, package_type, weight_grams, length_mm, width_mm, height_mm, products[].
items[].item_idstring, optionaloptional
Your own per-box reference (same key as v1). Must be unique across your orders; it is how you address the item afterwards.
curl -X POST 'https://api.starmile.io/api/v2/orders' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "service_id": 12,
    "order_id": "PO-10294",
    "customer_name": "Jane Doe",
    "customer_phone": "+994500000000",
    "items": [
      {
        "item_id": "BOX-1",
        "merchant_tracking": "MT-0001",
        "weight_grams": 1200,
        "products": [
          { "name": "Running shoes", "declared_value": 50, "currency": "USD", "quantity": 1 }
        ]
      }
    ]
  }'

Add an item

POST/api/v2/orders/{order_id}/items

Add a new item to an order that already has more than one item. The body is a single item in the same shape as one entry of the create items[]; the item carries no Starmile tracking of its own, so the 201 returns the order's tracking_number, your order_id, and the new item by its own references. A reused item_id, or a merchant_tracking held by a real item, is 422; an unidentified package already at the hub under that barcode is matched instead. Scope: orders:update.

An order created with a single item carries that box on the order itself and has no separate items to add to — adding one is refused 409. Create a new order for the additional package instead.

Body

item_idstringoptional
Optional. Your own reference for the item. Must be unique across your orders.
merchant_trackingstringoptional
Optional. The physical sticker code.
package_typestringoptional
Optional. One of: fragile, breakable, liquid.
weight_gramsintegeroptional
Optional. Declared gross weight (records-only).
length_mm / width_mm / height_mmintegeroptional
Optional. Declared dimensions in mm.
productsarrayoptional
Required. The item's contents (same shape as create).
curl -X POST 'https://api.starmile.io/api/v2/orders/PO-10294/items' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "item_id": "BOX-3",
    "merchant_tracking": "CN773300012345",
    "weight_grams": 1200,
    "products": [
      { "name": "Ceramic mug", "hs_code": "691200", "declared_value": 12.5, "currency": "USD", "quantity": 1 }
    ]
  }'

Update or cancel an item

PATCH/api/v2/orders/{order_id}/items/{item_id}

Both path segments are your references — the order_id and item_idyou sent on create. Same rules as v1's parcel update: only while the order is at its flow's first step, products replaces the full list, and a late merchant_tracking that matches a package already waiting at the hub is adopted, not rejected.

POST …/items/{item_id}/cancel cancels one item (and the order, when it was the last active one); POST /api/v2/orders/{order_id}/cancel cancels the whole order. Scopes: orders:update / orders:cancel.

curl -X PATCH 'https://api.starmile.io/api/v2/orders/PO-10294/items/BOX-1' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Content-Type: application/json' \
  -d '{ "weight_grams": 1500 }'

Split items onto a new order

POST/api/v2/orders/{order_id}/split

The v2 twin of v1's parcel split: peel one or more items out of a multi-item order and move them onto one fresh clone of the order (same service, flow, customer and destination). The body names the items with item_ids — an array of your own references (one or more, no duplicates) — and the new order with new_order_id, your own reference, so you can track and manage it like any other order. The 201follows the v2 wire: the new order's Starmile reference is tracking_number, order_id echoes your new_order_id, and each moved item carries no tracking of its own under items (item_id + merchant_tracking). Scope: orders:update.

Move a single item and the new order is a single-item order, stored the way v2 stores any single-item order — the item lives on the order itself, with no separate item to address (fetch its label and track it by its order_id, and you cannot add an item to it). Move two or more and the new order is a genuine multi-item order that keeps its items.

An ordinary order is split only while the items are at their flow's first step (else 409); a consolidation order can be split any time before its boxes are packed (an item already received or shelved can still be pulled out), and only a packed consolidation is refused 409. A folded single-item source order has no item to split off, and naming every item would leave the source empty — both 409; an item_id that names no active item is 404; a new_order_id already used, or equal to the source order_id, is 422.

Body

item_idsstring[]optional
Required. Your own item_id references for the items to move onto the new order — one or more, distinct. All move onto the one new order; the source keeps the rest.
new_order_idstringoptional
Required. Your own reference for the new order the items are moved onto. Must be unused across your orders and different from the source order_id.
reasonstringoptional
Optional free-text reason, kept on the new order's history.
curl -X POST 'https://api.starmile.io/api/v2/orders/PO-10294/split' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "item_ids": ["BOX-2", "BOX-3"],
    "new_order_id": "PO-10294-B",
    "reason": "ships separately"
  }'

Poll the status pool (v2)

GET/api/v2/partner/changes

Same polling model as Status updates: page with since, persist next_cursor, poll again immediately while has_more is true. Same optional filters (tracking_number, external_parent_id), same reason / reason_detail codes, and the feed still opens with the status the order was created in.

Row differences from v1: order_id is your own order reference, and item_id (set only on item-scoped rows, e.g. a single box reaching received_at_hub) replaces external_id.

# Poll from your last v2 cursor (NOT a v1 cursor — see the migration note)
curl 'https://api.starmile.io/api/v2/partner/changes?since=48210' \
  -H 'Authorization: Bearer <access_token>'

# One order's history, by YOUR own reference
curl 'https://api.starmile.io/api/v2/partner/changes?since=0&order_id=PO-10294' \
  -H 'Authorization: Bearer <access_token>'