Skip to content

Public Tracking ​

Use Public Tracking to show a customer the current safe projection for one shipment. The endpoint is anonymous and accepts only a tracking_number.

Call the endpoint ​

MethodPathResponse body
GET/public/tracking/{tracking_number}/JSON projection on 200; detail-only body on 404 or 429
HEAD/public/tracking/{tracking_number}/No body; the same existence and rate-limit status as GET

The complete URL is https://mexpress.nz/api/v1/public/tracking/{tracking_number}/. Encode the tracking number as one path segment. Do not substitute a package code, scan code, internal ID, or customer data.

bash
curl --fail-with-body 'https://mexpress.nz/api/v1/public/tracking/MX123456789/'

Use the response ​

The 200 object always contains these required keys. timeline may be an empty array, and nullable timestamps or context values may be null.

json
{
  "tracking_number": "MX123456789",
  "status": "out_for_delivery",
  "milestone": "out_for_delivery",
  "reason": null,
  "status_label": "Out for delivery",
  "package_count": 2,
  "package_summary": {
    "total": 2,
    "created": 0,
    "picked_up": 0,
    "in_warehouse": 0,
    "out_for_delivery": 2,
    "exception": 0,
    "returning": 0,
    "delivered": 0,
    "returned": 0,
    "cancelled": 0,
    "unresolved": 0
  },
  "projection_revision": 7,
  "occurred_at": "2026-07-02T03:04:05.000000Z",
  "timeline": []
}
FieldContract
tracking_numberRequired string. The requested shipment identifier.
statusRequired enum: created, in_transit, out_for_delivery, exception, delivered, returned, cancelled, completed.
milestoneNullable enum: information_received, picked_up, at_transit_facility, out_for_delivery, delivery_exception, returning_to_sender, delivered, returned_to_sender, cancelled, delivered_and_returned, partially_delivered, partially_returned.
reasonNullable enum: recipient_unavailable, address_issue, access_issue, recipient_refused, damaged, other_delivery_issue.
status_labelRequired string intended for customer-facing display.
package_countRequired non-negative integer.
projection_revisionRequired non-negative integer. Cache or compare it as a projection revision.
occurred_atNullable RFC 3339 date-time string for the current projection.
package_summaryRequired object with exactly the fixed keys below; every value is a non-negative integer.
timelineRequired array, possibly empty, of timeline events ordered oldest first.

package_summary always uses this key order: total, created, picked_up, in_warehouse, out_for_delivery, exception, returning, delivered, returned, cancelled, unresolved. Do not add keys or infer package identity from counts.

Each timeline item has required status (enum created, picked_up, in_warehouse, loaded, out_for_delivery, delivery_attempted, delivered, exception, return_initiated, returned_to_sender, cancelled, completed), label (string), and nullable occurred_at (date-time). An empty array means no timeline event is available.

Import the runnable examples ​

The canonical regions handle 200, detail-only 404, 429, and positive Retry-After without putting tracking data in a browser credential flow.

js
export async function fetchTracking(
  trackingNumber,
  { baseUrl = API_BASE_URL, fetchImpl = globalThis.fetch } = {},
) {
  const response = await fetchImpl(
    endpoint(baseUrl, `/public/tracking/${encodeURIComponent(trackingNumber)}/`),
    {
      method: "GET",
      headers: { Accept: "application/json" },
    },
  );
  if (response.status === 200) return { kind: "ok", data: await json(response) };
  if (response.status === 404) return { kind: "not_found" };
  if (response.status === 429) {
    const retryAfter = Number.parseInt(header(response.headers, "Retry-After") ?? "", 10);
    if (!Number.isInteger(retryAfter) || retryAfter < 1) {
      throw new Error("Tracking 429 did not include a positive Retry-After.");
    }
    return { kind: "rate_limited", retryAfter, error: await json(response) };
  }
  throw new Error(`Tracking request failed with HTTP ${response.status}.`);
}
py
def fetch_tracking(tracking_number: str, *, client, base_url: str = API_BASE_URL):
    response = client.get(
        _url(base_url, f"/public/tracking/{quote(tracking_number, safe='')}/"),
        headers={"Accept": "application/json"},
    )
    if response.status_code == 200:
        return {"kind": "ok", "data": response.json()}
    if response.status_code == 404:
        return {"kind": "not_found"}
    if response.status_code == 429:
        raw_retry_after = _header(response, "Retry-After")
        try:
            retry_after = int(raw_retry_after or "")
        except ValueError as error:
            raise ValueError("Tracking 429 did not include a positive Retry-After.") from error
        if retry_after < 1:
            raise ValueError("Tracking 429 did not include a positive Retry-After.")
        return {"kind": "rate_limited", "retry_after": retry_after, "error": response.json()}
    raise RuntimeError(f"Tracking request failed with HTTP {response.status_code}.")

Handle failures and polling ​

404 is deliberately detail-only. Show one neutral unavailable result and do not reveal whether the number is unknown, hidden, malformed, or belongs to another customer.

429 has a JSON body with detail equal to Too many public tracking requests. Retry after the indicated delay. and code equal to public_tracking_rate_limited. It also has a positive integer Retry-After header. Wait at least that long, then use backoff and jitter.

The anonymous limit is five requests per second per source IP with a burst of thirty, plus a shared service limit. Poll only when the customer view needs refresh; once per minute is a conservative default for one tracking number. Prefer Webhooks for backend updates.

Do not expose recipient contact or address data, package scan codes, POD media, exact device location, driver identity, billing data, raw scan evidence, or internal audit and risk fields. The response is a customer projection, not an operations feed.

Verify the flow ​

  • [ ] GET renders only the required documented fields.
  • [ ] HEAD is used only when no body is needed.
  • [ ] 404 is one neutral unavailable outcome.
  • [ ] 429 waits for a positive Retry-After.
  • [ ] Polling has backoff and jitter with no tight loop.

Continue with Webhooks for event-driven updates or POD API for authenticated evidence.

M Express server-to-server integration guide