API reference
Create an order
Hand one or more packages to Starmile in a single order. The order is your grouping (order_id); each parcel is a hub package with its own Starmile tracking number, and each parcel lists its products. Creating the order starts a logistics flow per parcel; the 201 returns the order with its parcels, and later status changes appear in your status pool.
Authentication is required (see Authentication). The order is created for the partner the credential belongs to, in the warehouse the credential is bound to — you never send those in the body.
Every order names a service_id — the id of a Service returned by GET /api/v1/services (read from your services & rates). The Service carries everything the order needs: the underlying flow, the corridor (origin & destination), and the delivery type— so you don’t send origin/destination.
You do not send a rate. Starmile resolves the applicable rate automatically and raises the invoice when the parcel reaches the status that rate is configured to bill on.
The delivery destination is bound to the service’s type: a Home Delivery service expects a parent_region + region, a Pudo Delivery service a pudo_id; Clearance and Cross Docking need no end-customer destination. A missing or invalid destination is rejected 422 with a message.
For Home Delivery you address the destination by your own reference: your parent region id/code in parent_region (e.g. "1") plus your leaf region id/code in region (e.g. "2"). Starmile maps that pair, per partner, to one of its regions — so your ids never need to match ours, and a leaf name that repeats across parents is disambiguated by the parent. The resolved region carries the delivery’s pricing tier.
If your reference is not mapped yet, the order is still accepted — it is not rejected. The response carries region_status: "mapped" when the region resolved, or "pending_mapping" when it did not. A pending_mapping order is queued for an operator to map your region in Starmile; once mapped, the waiting order is resolved automatically — you do not resend it. For a PUDO / locker / clearance service (no home region) region_status is "not_applicable".
Each entry in parcels[] is a physical package handled in the hub. item_id is your per-package reference — optional; when sent it is echoed back on the parcel as partner_tracking, and when omitted partner_tracking is left empty. merchant_trackingis the code on the sticker (also optional — set it later via the update endpoint, or omit it and Starmile fills it with the parcel’s own tracking number). Weights and dimensions are records-only; the hub re-measures (DWS) on arrival.
Your own references must be unique — your order_id, each parcel’s item_id, and each merchant_tracking. Reusing an item_id or a merchant_tracking that already exists under a different order (or repeating one across parcels in the same order) is rejected 422 with a message, so an order never duplicates.
One case is deliberately not a duplicate: a package that reached the hub before you registered it. It is received anyway, held as an unidentified package under the barcode on the box. When you later create the order with that same merchant_tracking, it is accepted and the waiting package is matched to it automatically — the parcel is already at the hub, so it goes straight to received_at_hub rather than waiting_for_arrival. You do not need to do anything differently: send the order as usual, even if the goods arrived first.
Re-sending an order_id you already used is safe: the endpoint is idempotent on your order_id. Nothing is created and the original order is replayed — 200 OK instead of 201 Created, with duplicate: true and the same order_id, region_status and items[] you got the first time. That is the fallback for a retry after a timeout or a double submit: you recover our ids instead of being locked out. The rest of the body is ignored on a replay — to change an accepted order, use the update-parcel endpoint below.
One Starmile tracking per parcel
order_id on create — and each parcel gets its own internally. You address a parcel by your own merchant_tracking (barcode); operators scan by that or the Starmile tracking.Create an order
/api/v1/ordersBody — order
service_idintegerrequired- The id of a Service from GET /api/v1/services (the Service entity — the flow is resolved from it). Must be published for your partner in this warehouse.
order_idstringrequired- Your reference for the order (max 255).
gov_idstringoptional- Recipient government ID — the AZ FIN or a foreign passport number — used for the customs declaration.
customer_namestringoptional- Recipient name (max 255).
customer_phonestringoptional- Recipient phone (max 64). At least one of customer_phone or customer_email is required — the customer must be reachable (delivery coordination, and any government-ID request).
customer_emailstringoptional- Recipient email. At least one of customer_email or customer_phone is required.
pudo_idintegeroptional- Required for a pudo-channel service: the id of a PUDO point in the destination country. The delivery channel (home / pudo / locker) is a property of the chosen service, not a field you send.
parent_regionstringoptional- Required for a Home Delivery service: YOUR own parent region id/code (e.g. "1"). With region, it forms the per-partner mapping key an operator maps to one of Starmile's regions; the parent disambiguates a leaf that repeats across parents.
regionstringoptional- Required for a Home Delivery service: YOUR own leaf region id/code (e.g. "2"). Resolved map-only, per partner, to one of Starmile's regions (no name/id fallback). An unmapped reference does NOT reject the order — it is accepted with region_status "pending_mapping" and resolved once an operator maps it. The resolved region carries the delivery's pricing tier.
address_firststringoptional- Address line 1 (home delivery).
address_secondstringoptional- Address line 2 (home delivery).
zipstringoptional- Postal / zip code (max 32).
notesstringoptional- Delivery notes for the courier (max 1000).
shipping_costnumberoptional- Optional. What you charge to ship this order, in the declared-value currency; it becomes the customs transport cost. Range 0–1,000,000. A zero (or omitted) shipping cost is treated as none — the order stores no value and customs sends a minimum at declaration time.
consolidation_requiredbooleanoptional- Optional. Ask to consolidate the order's parcels. The chosen service must enable consolidation — otherwise the order is rejected 422 ("Consolidation is not enabled for this service.").
Body — parcels[]
parcels[].item_idstringrequired- Your per-package reference (max 255). Unique within the order. Echoed back on the parcel as partner_tracking.
parcels[].merchant_trackingstringoptional- The code on the physical sticker (optional; settable later).
parcels[].package_typestringoptional- One of: fragile, breakable, liquid.
parcels[].weight_gramsintegeroptional- Declared gross weight (records-only; DWS re-measures).
parcels[].length_mm / width_mm / height_mmintegeroptional- Declared dimensions in mm (records-only).
Body — parcels[].products[]
products[].namestringrequired- Product name (max 255).
products[].hs_codestringoptional- Harmonised System code (max 32).
products[].declared_valuenumberoptional- Declared customs value of the product.
products[].currencystringoptional- ISO-4217 currency code (3 letters).
products[].quantityintegeroptional- Quantity (default 1).
products[].weight_gramsintegeroptional- Product weight in grams.
Response (201 created / 200 duplicate)
data.order_idstringoptional- The created order's Starmile tracking number. Use it as the order_id on the status and management endpoints.
data.duplicatebooleanoptional- true when this response replays an order that already existed for the order_id you sent — nothing was created by this request (status 200 instead of 201). false on a normal create.
data.region_statusstringoptional- Home Delivery region resolution: "mapped" (resolved), "pending_mapping" (accepted, awaiting an operator mapping — resolves automatically, do not resend), or "not_applicable" (PUDO / locker / clearance — no home region).
data.items[]arrayoptional- One entry per parcel: your item_id (as sent, or null if omitted) and parcel_id — our parcel's Starmile tracking number.
curl -X POST https://api.starmile.io/api/v1/orders \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"service_id": 4,
"order_id": "PO-10294",
"gov_id": "5AB12C3",
"customer_name": "Aysel M.",
"customer_phone": "+994500000000",
"parent_region": "1",
"region": "2",
"address_first": "Nizami 10",
"address_second": "Apt 4",
"zip": "AZ1000",
"notes": "Ring twice",
"shipping_cost": 9.50,
"parcels": [
{
"item_id": "PKG-1",
"merchant_tracking": "CN773300012345",
"package_type": "fragile",
"weight_grams": 1400,
"products": [
{ "name": "Sneakers", "hs_code": "640411", "declared_value": 120.00, "currency": "USD", "quantity": 1 }
]
}
]
}'Add a parcel
Add a new package to an order that already exists — when a shipment grows after you first created the order. One parcel per call, in the same shape as a single entry of parcels[] on create. The path uses your own order reference (order_id); the new parcel is created with its own Starmile tracking number and begins at waiting_for_arrivalon the order’s flow, independent of the other parcels’ progress. Requires the orders:update scope.
The 201 returns the order’s order_id and the new parcel’s item_id + parcel_id (its Starmile tracking number). Your references stay unique: an item_id you have already used, or a merchant_tracking already held by another of your parcels, is rejected 422. As on create, a merchant_tracking that belongs to a package already waiting at the hub (received before its record existed) is matched to it rather than rejected. A cancelled order can no longer take a new parcel: 409.
Add a parcel
/api/v1/orders/{order_id}/parcelsBody
item_idstringoptional- Optional. Your own reference for the parcel (records-only). Must be unique across your orders.
merchant_trackingstringoptional- Optional. The physical sticker code. Defaults to the parcel's Starmile tracking number when omitted.
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 parcel's contents (same shape as create) — needed for the customs declaration.
curl -X POST https://api.starmile.io/api/v1/orders/PO-10294/parcels \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"item_id": "PKG-3",
"merchant_tracking": "CN773300012345",
"package_type": "fragile",
"weight_grams": 1200,
"products": [
{ "name": "Ceramic mug", "hs_code": "691200", "declared_value": 12.5, "currency": "USD", "quantity": 1 }
]
}'Update a parcel
Change a parcel’s details while the order is still at its flow’s first step — most often to set the merchant_tracking (sticker code) once you have it. Partial: only the fields you send change; products, when sent, replaces the whole list. Once the order has moved past its first step (for example the parcel has been received at the hub) the update is rejected 409. Requires the orders:update scope. The path uses your own references: order_idthen the parcel’s item_id.
Setting the merchant_tracking late is the mirror of the create case above: if the package had already reached the hub under that barcode and was held as an unidentified package, sending it here matches the two together rather than failing. The parcel keeps its own tracking number, the reference it previously carried stays searchable, and — because the goods are already in the building — it goes straight to received_at_hub, which you will see on the status feed. A merchant_tracking already held by another of your parcels is still a conflict: 422 with a message, and nothing is changed.
Update a parcel
/api/v1/orders/{order_id}/parcels/{item_id}Body (all optional; partial update)
merchant_trackingstringoptional- The physical sticker code.
package_typestringoptional- One of: fragile, breakable, liquid.
weight_gramsintegeroptional- Declared gross weight (records-only).
length_mm / width_mm / height_mmintegeroptional- Declared dimensions in mm.
productsarrayoptional- Replaces the parcel's full product list (same shape as create).
curl -X PATCH https://api.starmile.io/api/v1/orders/PO-10294/parcels/PKG-1 \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"merchant_tracking": "CN773300099999",
"package_type": "liquid",
"products": [
{ "name": "Olive oil 1L", "hs_code": "150910", "declared_value": 18.0, "currency": "USD", "quantity": 2 }
]
}'Split parcels onto a new order
Peel one or more parcels out of a multi-parcel order and give them their own, separately trackable order — when packages need to ship apart from the rest (a different destination, their own declaration, or a grouping you want to undo). The parcels you name are detached and moved onto one fresh clone of the orderthat copies the source’s service, flow, customer and destination; each parcel keeps its own Starmile tracking number and continues its journey from wherever it already is, and the source keeps the parcels you did not name. Requires the orders:update scope. The path uses your own order_id; the body names the parcels with item_ids — an array of your own item_id references (one or more, no duplicates).
You name the new order with new_order_id — your own reference for the clone, so you can track and manage it exactly like any other order. The 201returns the new order’s Starmile tracking number as order_id, echoes your new_order_id and the source_order_id, and returns each moved parcel under items by its item_id + parcel_id. Both orders’ volumetric weight and declared value are recomputed from the parcels they now hold.
An ordinary order can be split while the parcels are still at their flow’s first step — the same window as update and cancel; once a named parcel has been received or moved past it, the split is rejected 409. A consolidation order has a wider window: it can be split any time before its boxes are packed, so a parcel that has already arrived at the hub (or been shelved for the consolidation) can still be pulled out — the parcel is taken off the consolidation and its new order continues on its own. Once the consolidation has been packed, its boxes can no longer be split off (409).
An order with a single package has nothing to split off, and naming every package would leave the source empty — both 409; a cancelled order, or a consolidation order that is already packed, is 409; an item_id that names no active parcel on the order is 404; a new_order_id you have already used, or one equal to the source order_id, is rejected 422.
Split parcels
/api/v1/orders/{order_id}/splitBody
item_idsstring[]optional- Required. Your own item_id references for the parcels 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 parcels 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/v1/orders/PO-10294/split \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"item_ids": ["PKG-2", "PKG-3"],
"new_order_id": "PO-10294-B",
"reason": "ships separately"
}'Cancel a parcel
Cancel a single parcel — one package of an order — while it is still at its flow’s first step. Once the parcel has been received / moved past the first step the cancel is rejected 409. When the cancelled parcel was the order’s last active parcel, the order is cancelled too. Idempotent for an already-cancelled parcel. Requires the orders:cancel scope. The path uses your own references: order_id then the parcel’s item_id.
Cancel a parcel
/api/v1/orders/{order_id}/parcels/{item_id}/cancelBody
reasonstringoptional- Optional free-text cancellation reason.
curl -X POST https://api.starmile.io/api/v1/orders/PO-10294/parcels/PKG-1/cancel \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{ "reason": "item out of stock" }'Cancel an order
Cancel an order while it is still at its flow’s first step — before any of its packages have moved past it (e.g. been received or handed to a carrier). Once any parcel has advanced past the first step the cancel is rejected 409. Idempotent for an already-cancelled order. Requires the orders:cancel scope.
Cancel an order
/api/v1/orders/{order_id}/cancelBody
reasonstringoptional- Optional free-text cancellation reason.
curl -X POST https://api.starmile.io/api/v1/orders/PO-10294/cancel \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{ "reason": "customer changed their mind" }'