API reference

Status updates

Follow your orders with a pull-based status pool. Each status change appends to a per-credential, cursor-ordered feed; you poll for everything since your last cursor. There is no outbound URL to register and nothing to retry — you are in control of when you read.

Replaces outbound webhooks

The status pool is the pull-based replacement for the previous outbound webhook delivery — there is no endpoint to register and nothing to retry. Requires the status:read scope.

Polling is stateless on our side: you persist the last next_cursor you processed and pass it back as since on the next call. Omit since on the first call to read from the beginning. Keep polling while has_more is true to drain a backlog, then poll on an interval (e.g. every few seconds) for new changes.

The feed is scoped to your credential — you only ever see status changes for packages that belong to you. Requires the status:read scope.

Only need one order? Pass tracking_number to narrow the feed to a single Starmile tracking number — the order (its order-level changes) or one parcel (its parcel-scoped changes). Combine it with since: start at since=0 and page while has_more is trueto reconstruct that order's full status history, without draining and filtering the entire feed.

Prefer your own ids? Pass external_parent_id — the reference you sent on create — instead, and you can follow an order without ever holding our tracking_number. It narrows the feed the same way and can be combined with tracking_number.

Most changes are order-level (external_id is null). Some milestones are reported per parcel — for example each parcel being received at the hub — and carry external_id, your own reference for that parcel (the item_id you sent on create). Because an order with several parcels reports each one separately, use external_id to act on the exact parcel; correlate it to the order with external_parent_id.

Each change carries the country(ISO-2) it occurred in — the hub's country — so on a cross-border journey you can tell an origin event (e.g. CN, received at the origin hub) apart from a destination one (e.g. AZ, arriving in the destination country).

An order's feed opens with the status it was created in— typically waiting_for_arrival, recorded the moment we accept your order and carrying previous_status: null. So the first thing you read for an order is always the point we took it on, not whichever hub event happened to come first.

When a change has a why — a parcel held at customs, a failed delivery attempt, a cancellation — it carries reason, a stable code from the catalogue below, and sometimes reason_detail, the free text a person wrote. Branch on reason; show reason_detail to a human. Both are null on a change that simply has no why, such as a parcel being received at a hub.

Codes are permanent: one is never renamed or reused, so a switchyou write today keeps working. New codes may be added, so treat an unrecognised one as "some other reason" rather than an error — and expect null too, since not every change comes with an explanation.

Poll the status pool

GET/api/v1/partner/changes

Query parameters

sinceintegeroptional
The last cursor you processed. Returns every change after it. Omit to start from the beginning.
limitintegeroptional
Max changes to return in one page (default applies if omitted).
tracking_numberstringoptional
Optional. Narrows the feed to a single Starmile tracking number — an order (its order-level changes) or one parcel (its parcel-scoped changes). Page from since=0 with the tracking number until has_more is false to get that subject's full history. Omit for the whole feed.
external_parent_idstringoptional
Optional. Narrows the feed to a single partner reference — the external_parent_id you sent on create — so you can track by your own id without holding our tracking number. Composes with the cursor and may be combined with tracking_number. Omit for the whole feed.

Response — data[]

cursorintegeroptional
Monotonic position of this change in your feed. Pass the latest back as since.
tracking_numberstringoptional
The Starmile tracking number the change is about — the parent order, or the individual parcel on a parcel-scoped change.
external_parent_idstringoptional
Your own reference for the order, echoed back.
external_idstring | nulloptional
Your own reference for a single parcel (the item_id you sent on create). Present only on parcel-scoped changes — a change on one parcel, such as a parcel received at the hub — so you can act on that exact parcel. null on order-level changes.
countrystring | nulloptional
ISO-2 country the change occurred in (the hub's country), so you can tell an origin event (e.g. CN) apart from a destination one (e.g. AZ). null on rows predating this field.
statusstringoptional
The new status the order (or parcel) moved to.
previous_statusstring | nulloptional
The status it moved from. null on the first change of an order — the one recorded when the order is created, which has nothing before it.
reasonstring | nulloptional
Why the change happened, as a stable code you can branch on — see the reason catalogue below. Present on the statuses that have a why (a customs hold, a failed delivery, a cancellation); null on the rest, and on changes whose reason we were not given.
reason_detailstring | nulloptional
The free text a person wrote alongside the reason — a courier's note, a cancellation explanation, or a reason you reported to us that is not in the catalogue. Show it to a human; never branch on it. Up to 255 characters, and null when nobody wrote anything.
occurred_atstringoptional
When the change happened, as a plain Y-m-d H:i:s timestamp (e.g. 2026-06-20 09:14:00). The zone it is expressed in is given by the timezone field — the two always agree.
timezonestringoptional
The IANA timezone occurred_at is expressed in (e.g. UTC). Use it to interpret occurred_at.

Reason catalogue — customs holds

missing_declarationcustoms_holdoptional
The parcel has no customs declaration.
inaccurate_informationcustoms_holdoptional
The declared information does not match the parcel.
prohibited_content_restricted_itemcustoms_holdoptional
The contents are prohibited or restricted for import.
commercial_quantity_personal_allowance_exceededcustoms_holdoptional
The shipment exceeds the recipient's personal allowance and is treated as commercial.

Reason catalogue — failed deliveries

customer_absentdelivery_failedoptional
Nobody was there to receive the parcel.
address_not_founddelivery_failedoptional
The courier could not find the address.
customer_refuseddelivery_failedoptional
The recipient refused the parcel.
could_not_reach_customerdelivery_failedoptional
The courier could not contact the recipient.
customer_no_answerdelivery_failedoptional
The courier reached the recipient's number or door, but nobody answered.
wrong_or_incomplete_addressdelivery_failedoptional
The address is wrong or missing details needed to deliver.
otp_verification_faileddelivery_failedoptional
The recipient could not confirm the one-time code at handover.
contact_info_incorrectdelivery_failedoptional
The phone number given for the recipient is wrong.
reschedule_requesteddelivery_failedoptional
The recipient asked to be delivered to on a later day.
pudo_pickup_requesteddelivery_failedoptional
The recipient asked to collect from a pick-up point instead.
courier_unable_to_deliverdelivery_failedoptional
The courier could not complete the delivery that day.

Reason catalogue — cancellations

cancelled_by_partnercancelledoptional
You cancelled the order or parcel through the API.
cancelled_by_customercancelledoptional
The recipient cancelled it themselves.
cancelled_by_operatorcancelledoptional
Cancelled on the recipient's behalf by an operator.

Response — envelope

next_cursorintegeroptional
Pass this as since on your next poll.
has_morebooleanoptional
true if more changes are waiting beyond this page — poll again immediately.
# Poll the whole feed from your last cursor
curl 'https://api.starmile.io/api/v1/partner/changes?since=10480' \
  -H 'Authorization: Bearer <access_token>'

# Or just one order/parcel's history — filter by its tracking number
curl 'https://api.starmile.io/api/v1/partner/changes?since=0&tracking_number=STM000123' \
  -H 'Authorization: Bearer <access_token>'

# Or track by your own reference — filter by the external_parent_id you sent
curl 'https://api.starmile.io/api/v1/partner/changes?since=0&external_parent_id=PO-10294' \
  -H 'Authorization: Bearer <access_token>'