Tracking number formats & auto-detection
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:
- Pass 1 — Strict
matches(n): Checks unambiguous prefixes, exact lengths, and check digits (ups1Z,s10,dhlExpress,gs1). The first carrier whosematches(n)returnstruewins. - Pass 2 — Looser
mayMatch(n): Only runs if no carrier matched in Pass 1. Catches secondary or partner formats:- UniUni (
UR…,U9999…,U000…) - GOFO (
GF+ 13 digits) - CDL Last Mile (hyphenated order numbers
^\d{9}-\d$) - AxleHire (Jitsu) (
SCD+ 16 chars, or 12/16-char mixed alphanumeric codes) - Purolator (3 letters + 9 digits
^[A-Z]{3}\d{9}$) - TForce Final Mile (depot order pieces
^[A-Z]{2}\d{7}-\d{1,2}$) - OSM Worldwide (22-char hub format
^A\d{10}[A-Z]{3}\d{8}$)
- UniUni (
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 (?carrier=glsus) or pre-USPS consolidator scans on 92… labels (?carrier=osm, ?carrier=pitneybowes)—pass ?carrier=<id>.
// 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 | ups | /^1Z[0-9A-Z]{16}$/ | — | ups1Z(n) (Mod-10) |
| 2 | USPS | usps | /^(420\d{5})?9[2345]\d{18,24}$/ or /^[A-Z]{2}\d{9}US$/ | — | s10(n) on …US |
| 3 | 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 | fedex | /^(\d{12}|\d{15}|\d{20}|96\d{20})$/ | — | — |
| 5 | Amazon | amazon | /^TB[ACM]\d{12}$/ | — | — |
| 6 | OnTrac / LaserShip | ontrac | /^([CD]\d{14}|1LS[A-Z0-9]{12,15}(-\d)?|L[AEHINX]\d{8})$/ | — | — |
| 7 | UniUni | uniuni | /^UUS[0-9A-Z]{13,16}$/ | /^(UR\d{12,20}|U9999\d{6,20}|U000\d{6,20})$/ | — |
| 8 | GOFO Express | gofo | /^GFUS\d{14}$/ | /^GF\d{13}$/ | — |
| 9 | Veho | veho | /^VH[0-9A-Z]{12}$/ | — | — |
| 10 | Spee-Dee | speedee | /^SP(\d{16}|\d{18})$/ | — | — |
| 11 | CDL Last Mile | cdl | /^CDL\d{11}$/ | /^\d{9}-\d$/ | — |
| 12 | Better Trucks | bettertrucks | /^(BTS_[0-9A-Z]{8,14}|BTP[0-9A-Z]{12})$/ | — | — |
| 13 | AxleHire (Jitsu) | axlehire | — | /^SCD[0-9A-Z]{16}$/ or 12/16-char mixed alphanumeric (!letters-then-digits) | — |
| 14 | Cirro | cirro | /^(CR\d{12}|CIFR\d{13})$/ | — | — |
| 15 | Yanwen | yanwen | /^[A-Z]{2}\d{9}YP$/ or /^YW[A-Z]{3}\d{12}$/ | — | s10(n) on …YP |
| 16 | 4PX | 4px | /^4PX\d{13}CN$/ | — | — |
| 17 | Cainiao | cainiao | /^(LP\d{14,17}|AP\d{14})$/ | — | — |
| 18 | YunExpress | yunexpress | /^YT\d{16}$/ | — | — |
| 19 | SpeedX | speedx | /^SPX([0-9A-Z]{15}|[0-9A-Z]{21})$/ | — | — |
| 20 | Intelcom (Dragonfly) | intelcom | /^INTLCM[A-Z]\d{9}$/ | — | — |
| 21 | Canada Post | canadapost | /^\d{16}$/ or /^[A-Z]{2}\d{9}CA$/ | — | gs1(n) on 16-digit; s10(n) on …CA |
| 22 | Purolator | purolator | — (12-digit PINs require ?carrier=purolator) | /^[A-Z]{3}\d{9}$/ | — |
| 23 | TForce Final Mile | tforce | /^TF\d{18}$/ | /^[A-Z]{2}\d{7}-\d{1,2}$/ | — |
| 24 | GLS US | glsus | — (never auto-detected; requires ?carrier=glsus) | — | — |
| 25 | OSM Worldwide | osm | /^OSM[0-9A-Z]{14}$/ | /^A\d{10}[A-Z]{3}\d{8}$/ (92… USPS barcodes require ?carrier=osm) | — |
| 26 | Pitney Bowes | pitneybowes | /^UPAA[A-Z]\d{15}$/ | — (92… USPS barcodes require ?carrier=pitneybowes) | — |
| 27 | CNE Express | cne | /^3A5V\d{9}$/ | — | — |
| 28 | SunYou | sunyou | /^SY[A-Z]{2}\d{9}$/ | — | — |
| 29 | UBI Smart Parcel | ubi | /^AU\d{13}$/ | — | — |
| 30 | Sendle | sendle | /^(QSS\d{9}|QS\d{6})$/ | — | — |
| 31 | 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):
/** 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 in Pass 1. If you are tracking a 12-digit Purolator or Canada Post PIN, pass?carrier=purolatoror?carrier=canadapost. (12-character alphanumeric Purolator PINs likePQR482019376auto-detect in Pass 2 via/^[A-Z]{3}\d{9}$/, while SendleQSS\d{9}numbers win earlier in Pass 1.) - 20-digit numbers starting
0034043(DHL's GS1 prefix,/^0034043\d{13}$/) route to DHL; all other 20-digit numbers route to FedEx.
2. 22-digit numbers (92…–95… vs 96…) & the "USPS Currently Awaiting Package" consolidator override
- 20–26 digit numbers starting
92,93,94, or95(optionally prefixed with420+ 5-digit destination ZIP,/^(420\d{5})?9[2345]\d{18,24}$/) route to USPS in Pass 1, while 22-digit numbers starting96(/^96\d{20}$/) route to FedEx. - Consolidator middle-mile scans (
?carrier=osmand?carrier=pitneybowes): Parcels shipped via OSM Worldwide or Pitney Bowes typically carry a92…(or420+ 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=osmor?carrier=pitneyboweson that same92…or420…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 (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) 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).
5. Partner & white-label numbers (?carrier= overrides)
Pass ?carrier= when tracking any of the following partner or shipper-assigned formats:
- GLS US: All GLS US / GSO numbers (
?carrier=glsus). - Purolator: 12-digit all-numeric PINs (
?carrier=purolator). - OSM Worldwide & Pitney Bowes:
92…/420+ZIP+92…USPS IMpb barcodes when querying middle-mile sort scans (?carrier=osmor?carrier=pitneybowes). - Asendia:
AS…USnumbers, USPS domestic induction labels, and destination postal numbers (?carrier=asendia). - UniUni: Partner numbers starting
GV…orJY…(?carrier=uniuni). - GOFO Express: Shipper numbers starting
CR…orYT…(?carrier=gofo). - Cirro: Parcels delivered by GOFO under a
GFUS…barcode auto-detect asgofo; pass?carrier=cirroto query Cirro directly, or forSKP…shipper numbers. - OnTrac, Veho, CDL, TForce Final Mile: 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 (…YP, YW…), 4PX (4PX…CN), Cainiao (LP…, AP…), YunExpress (YT + 16 digits), CNE Express (3A5V + 9 digits), SunYou (SY + 2 letters + 9 digits), or UBI Smart Parcel (AU + 13 digits)—as well as US outbound parcels via Asendia (AHOY + 8 chars)—are handed over to a destination carrier (such as USPS, UniUni, GOFO, SpeedX, OnTrac, Canada Post, Intelcom / Dragonfly, 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:
{
"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():
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.