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
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
/api/v1/partner/changesQuery 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>'