events[]: an append-only log of what the carrier said, kept lossless. It holds the raw code, raw text and raw location.status: a projection over that log into a small canonical set, which may be lossy, plusstatus_detailfor the reason.
status. Auditors and support agents read events[]. All disagreement between carriers is quarantined in the mapping from carrier codes to canonical status, and the implementer owns that mapping.
The diagram shows the two clocks: carrier data lands unchanged in events[], and only the implementer’s mapping turns it into status and status_detail.
1. The tracker object
2. Canonical status
Rules:
- Status is allowed to move backwards.
out_for_delivery→exception→out_for_deliveryis normal, and so isdelivered→in_transit, which happens when the carrier corrects a mis-scan. Clients MUST NOT assume the status only moves forward. statusequals the status of the most recent event byoccurred_at, except thatunknownevents never override a known status.delivered,returnedandcancelledstop automatic refresh after 72 hours without new events. “Terminal” means exactly that. A carrier correction can still move a terminal status, as described above.- Every event carries both canonical and carrier fields.
carrier_codeis never dropped.
status follows whatever the most recent event says.
status_detail
A finer reason within the status. Clients MAY ignore it, but they MUST tolerate unknown values.
Why
delivery_attempted is an exception. One carrier treats a failed attempt as a normal step and another treats it as an exception. The spec takes the recipient’s view: something needs attention. The retry is then a further out_for_delivery event.
3. Create a tracker
Idempotent by nature. Creating a tracker for a
(carrier, tracking_number) that the account already tracks returns the existing tracker with 200 and messages[0].code = "tracker_exists". This holds even without an Idempotency-Key.
tracking_number_not_found is not an error at create time. New labels are often unknown to the carrier for hours. The tracker is created with status: unknown, status_detail: not_found_yet, and keeps polling for up to 30 days before it stops with refresh.stopped_reason = "not_found".
The sequence shows a tracker created for a number the carrier does not know yet.
4. Discard a tracker
purchased (409 state_error / tracker_owned_by_shipment). Use POST /v1/trackers/{id} with refresh: { mode: "manual" } to stop polling instead. When the shipment is voided, its tracker stops automatically.
The flowchart shows how a discard request is decided.
5. Refresh
429 otherwise). It is useful for “where is my order” pages. It is not useful as a polling loop. Use webhooks for that.
6. Events, list, search, update
tracker.events inlines the 50 most recent events. The full log is at /events.
7. Sources
tracking_event.source records where an event came from:
Events from different sources are merged by
occurred_at. Duplicates (same carrier_code, same occurred_at within 60 seconds, same location) are collapsed, and the first-received copy wins.
The flowchart shows events from all five sources merging into one tracker log.
8. Proof of delivery
Whendelivered, proof_of_delivery MAY be { signed_by, signature_file, photo_files: ["file_..."], location: { latitude, longitude }, delivered_to: "front_door" }. The image files are file objects with purpose: "proof_of_delivery".
9. Events (webhooks)
tracker.created, tracker.updated (any new event), tracker.status_changed (canonical status changed, which is the one most clients want), tracker.delivered, tracker.exception, tracker.returned, tracker.stopped, tracker.deleted.