# Tracking number formats & auto-detection

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

When you call `GET /v1/track/:number` without `?carrier=`, `lastscanned` identifies the carrier from the tracking number's prefix, length, and check digit before making any network request.

Because detection runs deterministically ahead of the lookup, a mistyped UPS `1Z`, 16-digit Canada Post PIN, or UPU S10 international number fails fast with `400 unknown_carrier` instead of wasting a lookup or polling the wrong carrier.

## How two-pass detection works

`detect(trackingNumber)` upper-cases and trims the input, then evaluates all 31 carriers in fixed precedence order across two passes:

1. **Pass 1 — Strict `matches(n)`**: Checks unambiguous prefixes, exact lengths, and check digits (`ups1Z`, `s10`, `dhlExpress`, `gs1`). The first carrier whose `matches(n)` returns `true` wins.
2. **Pass 2 — Looser `mayMatch(n)`**: Only runs if no carrier matched in Pass 1. Catches secondary or partner formats:
   - [UniUni](/carriers/uniuni) (`UR…`, `U9999…`, `U000…`)
   - [GOFO](/carriers/gofo) (`GF` + 13 digits)
   - [CDL Last Mile](/carriers/cdl) (hyphenated order numbers `^\d{9}-\d$`)
   - [AxleHire (Jitsu)](/carriers/axlehire) (`SCD` + 16 chars, or 12/16-char mixed alphanumeric codes)
   - [Purolator](/carriers/purolator) (3 letters + 9 digits `^[A-Z]{3}\d{9}$`)
   - [TForce Final Mile](/carriers/tforce) (depot order pieces `^[A-Z]{2}\d{7}-\d{1,2}$`)
   - [OSM Worldwide](/carriers/osm) (22-char hub format `^A\d{10}[A-Z]{3}\d{8}$`)

**One carrier per number.** Detection sends the number to the single winning carrier and never falls back to another carrier on `404 not_found`. To override detection—or to look up a carrier that has no auto-detected format like [GLS US](/carriers/glsus) (`?carrier=glsus`) or pre-USPS consolidator scans on `92…` labels (`?carrier=osm`, `?carrier=pitneybowes`)—pass `?carrier=<id>`.

```ts
// Evaluating carriers in exact precedence order:
// ups → usps → dhl → fedex → amazon → ontrac → uniuni → gofo → veho →
// speedee → cdl → bettertrucks → axlehire → cirro → yanwen → 4px → cainiao → yunexpress →
// speedx → intelcom → canadapost → purolator → tforce → glsus → osm → pitneybowes → cne → sunyou → ubi → sendle → asendia
export function detect(trackingNumber: string): Carrier | undefined {
  const n = trackingNumber.trim().toUpperCase();
  const all = Object.values(carriers);
  return all.find((c) => c.matches(n)) ?? all.find((c) => c.mayMatch?.(n));
}
```

---

## Complete regex & prefix reference (all 31 carriers)

The table below shows the exact regular expressions and check-digit functions used in production (`n` is already trimmed and upper-cased).

| Order | Carrier | `id` | Pass 1: Strict `matches(n)` | Pass 2: Fallback `mayMatch(n)` | Check digit |
|---|---|---|---|---|---|
| 1 | [UPS](/carriers/ups) | `ups` | `/^1Z[0-9A-Z]{16}$/` | — | `ups1Z(n)` (Mod-10) |
| 2 | [USPS](/carriers/usps) | `usps` | `/^(420\d{5})?9[2345]\d{18,24}$/` or `/^[A-Z]{2}\d{9}US$/` | — | `s10(n)` on `…US` |
| 3 | [DHL](/carriers/dhl) | `dhl` | `/^\d{10}$/`, `/^(JJ?D\d{10,20}\|GM\d{16,20}\|0034043\d{13})$/`, or `/^[A-Z]{2}\d{9}DE$/` | — | `dhlExpress(n)` on 10-digit; `s10(n)` on `…DE` |
| 4 | [FedEx](/carriers/fedex) | `fedex` | `/^(\d{12}\|\d{15}\|\d{20}\|96\d{20})$/` | — | — |
| 5 | [Amazon](/carriers/amazon) | `amazon` | `/^TB[ACM]\d{12}$/` | — | — |
| 6 | [OnTrac / LaserShip](/carriers/ontrac) | `ontrac` | `/^([CD]\d{14}\|1LS[A-Z0-9]{12,15}(-\d)?\|L[AEHINX]\d{8})$/` | — | — |
| 7 | [UniUni](/carriers/uniuni) | `uniuni` | `/^UUS[0-9A-Z]{13,16}$/` | `/^(UR\d{12,20}\|U9999\d{6,20}\|U000\d{6,20})$/` | — |
| 8 | [GOFO Express](/carriers/gofo) | `gofo` | `/^GFUS\d{14}$/` | `/^GF\d{13}$/` | — |
| 9 | [Veho](/carriers/veho) | `veho` | `/^VH[0-9A-Z]{12}$/` | — | — |
| 10 | [Spee-Dee](/carriers/speedee) | `speedee` | `/^SP(\d{16}\|\d{18})$/` | — | — |
| 11 | [CDL Last Mile](/carriers/cdl) | `cdl` | `/^CDL\d{11}$/` | `/^\d{9}-\d$/` | — |
| 12 | [Better Trucks](/carriers/bettertrucks) | `bettertrucks` | `/^(BTS_[0-9A-Z]{8,14}\|BTP[0-9A-Z]{12})$/` | — | — |
| 13 | [AxleHire (Jitsu)](/carriers/axlehire) | `axlehire` | — | `/^SCD[0-9A-Z]{16}$/` or 12/16-char mixed alphanumeric (`!letters-then-digits`) | — |
| 14 | [Cirro](/carriers/cirro) | `cirro` | `/^(CR\d{12}\|CIFR\d{13})$/` | — | — |
| 15 | [Yanwen](/carriers/yanwen) | `yanwen` | `/^[A-Z]{2}\d{9}YP$/` or `/^YW[A-Z]{3}\d{12}$/` | — | `s10(n)` on `…YP` |
| 16 | [4PX](/carriers/4px) | `4px` | `/^4PX\d{13}CN$/` | — | — |
| 17 | [Cainiao](/carriers/cainiao) | `cainiao` | `/^(LP\d{14,17}\|AP\d{14})$/` | — | — |
| 18 | [YunExpress](/carriers/yunexpress) | `yunexpress` | `/^YT\d{16}$/` | — | — |
| 19 | [SpeedX](/carriers/speedx) | `speedx` | `/^SPX([0-9A-Z]{15}\|[0-9A-Z]{21})$/` | — | — |
| 20 | [Intelcom (Dragonfly)](/carriers/intelcom) | `intelcom` | `/^INTLCM[A-Z]\d{9}$/` | — | — |
| 21 | [Canada Post](/carriers/canadapost) | `canadapost` | `/^\d{16}$/` or `/^[A-Z]{2}\d{9}CA$/` | — | `gs1(n)` on 16-digit; `s10(n)` on `…CA` |
| 22 | [Purolator](/carriers/purolator) | `purolator` | — (12-digit PINs require `?carrier=purolator`) | `/^[A-Z]{3}\d{9}$/` | — |
| 23 | [TForce Final Mile](/carriers/tforce) | `tforce` | `/^TF\d{18}$/` | `/^[A-Z]{2}\d{7}-\d{1,2}$/` | — |
| 24 | [GLS US](/carriers/glsus) | `glsus` | — (**never auto-detected**; requires `?carrier=glsus`) | — | — |
| 25 | [OSM Worldwide](/carriers/osm) | `osm` | `/^OSM[0-9A-Z]{14}$/` | `/^A\d{10}[A-Z]{3}\d{8}$/` (`92…` USPS barcodes require `?carrier=osm`) | — |
| 26 | [Pitney Bowes](/carriers/pitneybowes) | `pitneybowes` | `/^UPAA[A-Z]\d{15}$/` | — (`92…` USPS barcodes require `?carrier=pitneybowes`) | — |
| 27 | [CNE Express](/carriers/cne) | `cne` | `/^3A5V\d{9}$/` | — | — |
| 28 | [SunYou](/carriers/sunyou) | `sunyou` | `/^SY[A-Z]{2}\d{9}$/` | — | — |
| 29 | [UBI Smart Parcel](/carriers/ubi) | `ubi` | `/^AU\d{13}$/` | — | — |
| 30 | [Sendle](/carriers/sendle) | `sendle` | `/^(QSS\d{9}\|QS\d{6})$/` | — | — |
| 31 | [Asendia](/carriers/asendia) | `asendia` | `/^AHOY[0-9A-Z]{8}$/` | — (USPS, `AS…US` & destination numbers require `?carrier=asendia`) | — |

---

## Check-digit algorithms

Four check-digit validators rule out mistyped numbers and non-tracking numbers (such as 10-digit phone numbers that would otherwise look like a 10-digit DHL Express waybill, or 16-digit credit card numbers that would otherwise look like a Canada Post PIN):

```ts
/** UPS 1Z: letters count as (char code - 63) mod 10, odd indices doubled, mod 10. */
export function ups1Z(n: string): boolean {
  const body = n.slice(2, -1).toUpperCase();
  let sum = 0;
  for (let i = 0; i < body.length; i++) {
    const ch = body[i];
    const v = ch >= "0" && ch <= "9" ? Number(ch) : (ch.charCodeAt(0) - 63) % 10;
    sum += i % 2 ? v * 2 : v;
  }
  return (10 - (sum % 10)) % 10 === Number(n.slice(-1));
}

/** UPU S10 (EA123456785US, RR123456785DE, UV123456785YP, EE123456785CA): weights 8,6,4,2,3,5,9,7 over 8 serial digits, mod 11. */
export function s10(n: string): boolean {
  const d = n.slice(2, 11);
  const w = [8, 6, 4, 2, 3, 5, 9, 7];
  const c = 11 - (w.reduce((s, x, i) => s + x * Number(d[i]), 0) % 11);
  return (c === 10 ? 0 : c === 11 ? 5 : c) === Number(d[8]);
}

/** DHL Express 10-digit waybill: first 9 digits mod 7 equal the 10th digit. */
export function dhlExpress(n: string): boolean {
  return Number(n.slice(0, 9)) % 7 === Number(n[9]);
}

/** GS1 mod 10 (Canada Post 16-digit PINs, SSCC, GTIN): weights 3,1 from the right, check digit excluded. */
export function gs1(n: string): boolean {
  const body = n.slice(0, -1);
  let sum = 0;
  for (let i = 0; i < body.length; i++) sum += Number(body[body.length - 1 - i]) * (i % 2 ? 1 : 3);
  return (10 - (sum % 10)) % 10 === Number(n.slice(-1));
}
```

---

## Format collisions & precedence rules

Several carriers issue overlapping numeric lengths or hand parcels to partner networks. Here is how `lastscanned` resolves them:

### 1. 12-digit and 20-digit numbers: FedEx vs Purolator, Canada Post & DHL Paket
- **12-digit numbers** (`/^\d{12}$/`) route to [FedEx](/carriers/fedex) in Pass 1. If you are tracking a 12-digit [Purolator](/carriers/purolator) or [Canada Post](/carriers/canadapost) PIN, pass `?carrier=purolator` or `?carrier=canadapost`. (12-character alphanumeric Purolator PINs like `PQR482019376` auto-detect in Pass 2 via `/^[A-Z]{3}\d{9}$/`, while [Sendle](/carriers/sendle) `QSS\d{9}` numbers win earlier in Pass 1.)
- **20-digit numbers starting `0034043`** (DHL's GS1 prefix, `/^0034043\d{13}$/`) route to [DHL](/carriers/dhl); all other 20-digit numbers route to [FedEx](/carriers/fedex).

### 2. 22-digit numbers (`92…`–`95…` vs `96…`) & the `"USPS Currently Awaiting Package"` consolidator override
- **20–26 digit numbers starting `92`, `93`, `94`, or `95`** (optionally prefixed with `420` + 5-digit destination ZIP, `/^(420\d{5})?9[2345]\d{18,24}$/`) route to [USPS](/carriers/usps) in Pass 1, while **22-digit numbers starting `96`** (`/^96\d{20}$/`) route to [FedEx](/carriers/fedex).
- **Consolidator middle-mile scans (`?carrier=osm` and `?carrier=pitneybowes`)**: Parcels shipped via [OSM Worldwide](/carriers/osm) or [Pitney Bowes](/carriers/pitneybowes) typically carry a `92…` (or `420` + ZIP + `92…`) USPS IMpb barcode. While the parcel is moving through OSM's or Pitney Bowes's middle-mile sort network (3–7 days), USPS only displays *"Pre-Shipment: Shipping Label Created, USPS Awaiting Item"* (`"USPS Currently Awaiting Package"`). Pass **`?carrier=osm`** or **`?carrier=pitneybowes`** on that same `92…` or `420…92…` number to retrieve the consolidator's live regional sort facility scans before USPS acceptance.

### 3. Why GLS US (`?carrier=glsus`) is never auto-detected
[GLS US](/carriers/glsus) (formerly Golden State Overnight / GSO) uses 8–11 digit uncheck-digited numbers (typically 9 digits like `512345678`) and arbitrary shipper references (`CA0W2208048`, 20-digit references) that collide with other carriers and get reused across shipments. `glsus` sets `matches: () => false` and has no `mayMatch`, so you must always pass **`?carrier=glsus`** (or `carrier: "glsus"` in MCP).

### 4. Alphanumeric 12- and 16-character codes: AxleHire (Jitsu) vs prefixed carriers
[AxleHire (Jitsu)](/carriers/axlehire) issues 12- and 16-character alphanumeric codes (e.g. `4AUMF4FWVRB5`, `BQH4FN5QAQYV3SDQ`) as well as `SCD` + 16-character shipper numbers. To prevent AxleHire from swallowing prefixed carrier numbers like `VH` + 12 (`veho`), `BTP` + 12 (`bettertrucks`), `CR` + 12 (`cirro`), `QSS` + 9 (`sendle`), or `AHOY` + 8 (`asendia`):
- AxleHire only runs in **Pass 2 (`mayMatch`)**, after all Pass 1 prefix carriers have been checked.
- AxleHire explicitly rejects pure letters-then-digits strings (`!/^[A-Z]+\d+$/i.test(n)`), requiring letters and digits to be interleaved (which also leaves 3-letters-then-9-digits cleanly to [Purolator](/carriers/purolator)).

### 5. Partner & white-label numbers (`?carrier=` overrides)
Pass `?carrier=` when tracking any of the following partner or shipper-assigned formats:
- **[GLS US](/carriers/glsus)**: All GLS US / GSO numbers (`?carrier=glsus`).
- **[Purolator](/carriers/purolator)**: 12-digit all-numeric PINs (`?carrier=purolator`).
- **[OSM Worldwide](/carriers/osm) & [Pitney Bowes](/carriers/pitneybowes)**: `92…` / `420+ZIP+92…` USPS IMpb barcodes when querying middle-mile sort scans (`?carrier=osm` or `?carrier=pitneybowes`).
- **[Asendia](/carriers/asendia)**: `AS…US` numbers, USPS domestic induction labels, and destination postal numbers (`?carrier=asendia`).
- **[UniUni](/carriers/uniuni)**: Partner numbers starting `GV…` or `JY…` (`?carrier=uniuni`).
- **[GOFO Express](/carriers/gofo)**: Shipper numbers starting `CR…` or `YT…` (`?carrier=gofo`).
- **[Cirro](/carriers/cirro)**: Parcels delivered by GOFO under a `GFUS…` barcode auto-detect as `gofo`; pass `?carrier=cirro` to query Cirro directly, or for `SKP…` shipper numbers.
- **[OnTrac](/carriers/ontrac), [Veho](/carriers/veho), [CDL](/carriers/cdl), [TForce Final Mile](/carriers/tforce)**: Shipper-assigned reference numbers outside the standard barcode formats above.

---

## Cross-border handoffs (`yanwen`, `4px`, `cainiao`, `yunexpress`, `cne`, `sunyou`, `ubi`, `asendia`)

Cross-border parcels originating in China via **[Yanwen](/carriers/yanwen)** (`…YP`, `YW…`), **[4PX](/carriers/4px)** (`4PX…CN`), **[Cainiao](/carriers/cainiao)** (`LP…`, `AP…`), **[YunExpress](/carriers/yunexpress)** (`YT` + 16 digits), **[CNE Express](/carriers/cne)** (`3A5V` + 9 digits), **[SunYou](/carriers/sunyou)** (`SY` + 2 letters + 9 digits), or **[UBI Smart Parcel](/carriers/ubi)** (`AU` + 13 digits)—as well as US outbound parcels via **[Asendia](/carriers/asendia)** (`AHOY` + 8 chars)—are handed over to a destination carrier (such as **[USPS](/carriers/usps)**, **[UniUni](/carriers/uniuni)**, **[GOFO](/carriers/gofo)**, **[SpeedX](/carriers/speedx)**, **[OnTrac](/carriers/ontrac)**, **[Canada Post](/carriers/canadapost)**, **[Intelcom / Dragonfly](/carriers/intelcom)**, **Australia Post**, or **Royal Mail**) after clearing customs.

When you query the origin cross-border number, `lastscanned` returns the full scan timeline and populates the top-level `message` field with the destination carrier and secondary tracking number as soon as the handoff occurs:

```json
{
  "carrier": "yunexpress",
  "trackingNumber": "YT2628500703004876",
  "status": "in_transit",
  "statusDescription": "Arrived at destination airport",
  "message": "Handed to UniUni as UUS5BM1269395824723",
  "events": [ ... ]
}
```

You can also query the domestic last-mile tracking number (`UUS5BM1269395824723`, `SPX…`, `INTLCM…`, `7012345678901230`, `GFUS…`, or `420…92…`) directly with `GET /v1/track/:number` to inspect the local delivery scans.

---

## Skip maintaining regexes and fighting carrier 403s in your own app

When you query UPS, FedEx, USPS, DHL, OnTrac, Veho, Canada Post, Purolator, or Sendle directly from a serverless or edge backend (Cloudflare Workers, Vercel, AWS Lambda, Next.js), datacenter requests run straight into Akamai Bot Manager `403`/`428` blocks, Cloudflare Turnstile challenges, Veho Firebase App Check token gates, Sendle SHA-256 proof-of-work challenges, or enterprise OAuth / shipper contract portals. Instead of maintaining carrier regex tables, check-digit validators, per-carrier OAuth credentials, and 31 separate response parsers in your own codebase, send the raw tracking number to `GET /v1/track/:number` in one `fetch()`:

```ts
const res = await fetch(`https://api.lastscanned.com/v1/track/${encodeURIComponent(number)}`, {
  headers: { Authorization: `Bearer ${process.env.LASTSCANNED_KEY}` },
});
const data = await res.json();
// data.carrier is normalized ("ups" | "usps" | "fedex" | ...), and data.status is one of 7 standard states.
```
