# Tracking

URL: https://docs.lastscanned.com/tracking

## One number

```bash tab="curl"
curl https://api.lastscanned.com/v1/track/1Z999AA10123456784 \
  -H "Authorization: Bearer $LASTSCANNED_KEY"
```

```js tab="JavaScript"
const res = await fetch("https://api.lastscanned.com/v1/track/1Z999AA10123456784", {
  headers: { Authorization: `Bearer ${process.env.LASTSCANNED_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error.message);
const tracking = await res.json();
```

```python tab="Python"
import os, requests

res = requests.get(
    "https://api.lastscanned.com/v1/track/1Z999AA10123456784",
    headers={"Authorization": f"Bearer {os.environ['LASTSCANNED_KEY']}"},
)
res.raise_for_status()
tracking = res.json()
```

Returns a [tracking result](#the-response), or an [error](/errors): `404 not_found` when the carrier has no record, `400 unknown_carrier` when no carrier's format matches.

## Several numbers

`GET /v1/track?numbers=a,b,c` takes up to 100 numbers, at most 35 for any one carrier. Numbers from different carriers can be mixed; duplicates are removed.

```bash tab="curl"
curl "https://api.lastscanned.com/v1/track?numbers=1Z999AA10123456784,123456789012" \
  -H "Authorization: Bearer $LASTSCANNED_KEY"
```

```js tab="JavaScript"
const numbers = ["1Z999AA10123456784", "123456789012"];
const res = await fetch(`https://api.lastscanned.com/v1/track?numbers=${numbers.join(",")}`, {
  headers: { Authorization: `Bearer ${process.env.LASTSCANNED_KEY}` },
});
const { results } = await res.json();
```

```python tab="Python"
import os, requests

res = requests.get(
    "https://api.lastscanned.com/v1/track",
    params={"numbers": ",".join(["1Z999AA10123456784", "123456789012"])},
    headers={"Authorization": f"Bearer {os.environ['LASTSCANNED_KEY']}"},
)
results = res.json()["results"]
```

```json
{
  "results": [
    { "trackingNumber": "1Z999AA10123456784", "result": { "carrier": "ups", "status": "in_transit", "events": [] } },
    { "trackingNumber": "123456789012", "error": { "code": "not_found", "message": "FedEx returned no track details for that number" } }
  ]
}
```

- One entry per number, in the order sent, each with `result` (same shape as a single lookup) or `error`. One bad number never fails the others.
- The call returns `200` unless it is malformed (`400`), unauthorized (`401`) or over the daily limit (`429`).
- The response comes back when every number has an answer. Each carrier's numbers are looked up together.
- `x-cache-hits` is how many numbers came from cache.

## Carrier detection

The carrier is worked out from the number's format, and its check digit where the format has one, so a typo is turned away with `unknown_carrier` instead of being looked up. Formats are listed in [Carriers](/carriers).

**One carrier per number.** Detection picks exactly one carrier and never falls back to another. If that carrier has no record, the answer is `not_found`.

Pass `carrier` to choose the carrier yourself and skip detection:

```bash
curl "https://api.lastscanned.com/v1/track/123456789012?carrier=fedex" \
  -H "Authorization: Bearer $LASTSCANNED_KEY"
```

Use it when:

- the number is too generic to detect (OnTrac references, some UniUni and GOFO partner numbers, shippers' own numbers on Veho, CDL shipper references, most AxleHire codes);
- two carriers share a format and detection picked the wrong one (20-digit DHL Paket numbers not starting `0034043` go to FedEx; Cirro's `GFUS…` numbers go to GOFO).

On the batch route, `carrier` applies to every number. Carrier ids come from [`GET /v1/carriers`](/carriers#carrier-ids).

## The response

| Field | |
|---|---|
| `carrier` | Carrier id, e.g. `ups`. |
| `trackingNumber` | Upper-cased, trimmed. |
| `status` | `label_created`, `in_transit`, `out_for_delivery`, `delivered`, `exception`, `returned` or `unknown`. |
| `statusDescription` | The carrier's wording of the status. |
| `service` | e.g. `UPS Ground`. Can be `null`. |
| `deliveredAt` | Delivery time in UTC, or `null`. |
| `deliveredAtLocal` | Delivery time as the carrier printed it, no zone. |
| `signedBy` | Can be `null`. |
| `destination` | `{ city, state, country }`, any of them `null`; or `null`. |
| `estimatedDelivery` | Expected delivery as local time with no zone, often a date only. `null` or absent if the carrier gives none. |
| `estimatedDeliveryAt` | The same moment in UTC, only when the carrier states its zone (FedEx, DHL). |
| `message` | The carrier's latest-update sentence, e.g. why a delivery is late. Cross-border carriers name the handoff carrier and its number here (`Handed to USPS as 4207…`). |
| `events` | Scans, newest first. |

Each event has `status`, `description`, `location` (or `null`), `code` (the carrier's event code; `null` for USPS) and two times:

- `timestamp`: UTC, ISO 8601 with `Z`. Use this one. It is `null` when the carrier gave no time of day or no zone to convert from.
- `localTime`: the time as the carrier printed it, no zone (`2026-08-07T16:17:00`). A date alone means no time of day was given. Always `null` for Amazon.

Where `timestamp` comes from:

- **UPS, Amazon**: UTC from the carrier.
- **FedEx, OnTrac, UniUni, GOFO, Veho**: the carrier gives a zone with each scan, so the conversion is exact. A UniUni "order received" scan has no zone and no `timestamp`.
- **DHL**: Express usually gives an offset. eCommerce and Parcel often don't, and `timestamp` is then `null`.
- **USPS**: prints no zone. `timestamp` assumes the zone of the location the page was loaded from; USPS doesn't state it.
- **Cirro, YunExpress**: only the newest scan has a `timestamp`.
