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:

  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:

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).

OrderCarrieridPass 1: Strict matches(n)Pass 2: Fallback mayMatch(n)Check digit
1UPSups/^1Z[0-9A-Z]{16}$/—ups1Z(n) (Mod-10)
2USPSusps/^(420\d{5})?9[2345]\d{18,24}$/ or /^[A-Z]{2}\d{9}US$/—s10(n) on …US
3DHLdhl/^\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
4FedExfedex/^(\d{12}|\d{15}|\d{20}|96\d{20})$/——
5Amazonamazon/^TB[ACM]\d{12}$/——
6OnTrac / LaserShipontrac/^([CD]\d{14}|1LS[A-Z0-9]{12,15}(-\d)?|L[AEHINX]\d{8})$/——
7UniUniuniuni/^UUS[0-9A-Z]{13,16}$//^(UR\d{12,20}|U9999\d{6,20}|U000\d{6,20})$/—
8GOFO Expressgofo/^GFUS\d{14}$//^GF\d{13}$/—
9Vehoveho/^VH[0-9A-Z]{12}$/——
10Spee-Deespeedee/^SP(\d{16}|\d{18})$/——
11CDL Last Milecdl/^CDL\d{11}$//^\d{9}-\d$/—
12Better Trucksbettertrucks/^(BTS_[0-9A-Z]{8,14}|BTP[0-9A-Z]{12})$/——
13AxleHire (Jitsu)axlehire—/^SCD[0-9A-Z]{16}$/ or 12/16-char mixed alphanumeric (!letters-then-digits)—
14Cirrocirro/^(CR\d{12}|CIFR\d{13})$/——
15Yanwenyanwen/^[A-Z]{2}\d{9}YP$/ or /^YW[A-Z]{3}\d{12}$/—s10(n) on …YP
164PX4px/^4PX\d{13}CN$/——
17Cainiaocainiao/^(LP\d{14,17}|AP\d{14})$/——
18YunExpressyunexpress/^YT\d{16}$/——
19SpeedXspeedx/^SPX([0-9A-Z]{15}|[0-9A-Z]{21})$/——
20Intelcom (Dragonfly)intelcom/^INTLCM[A-Z]\d{9}$/——
21Canada Postcanadapost/^\d{16}$/ or /^[A-Z]{2}\d{9}CA$/—gs1(n) on 16-digit; s10(n) on …CA
22Purolatorpurolator— (12-digit PINs require ?carrier=purolator)/^[A-Z]{3}\d{9}$/—
23TForce Final Miletforce/^TF\d{18}$//^[A-Z]{2}\d{7}-\d{1,2}$/—
24GLS USglsus— (never auto-detected; requires ?carrier=glsus)——
25OSM Worldwideosm/^OSM[0-9A-Z]{14}$//^A\d{10}[A-Z]{3}\d{8}$/ (92… USPS barcodes require ?carrier=osm)—
26Pitney Bowespitneybowes/^UPAA[A-Z]\d{15}$/— (92… USPS barcodes require ?carrier=pitneybowes)—
27CNE Expresscne/^3A5V\d{9}$/——
28SunYousunyou/^SY[A-Z]{2}\d{9}$/——
29UBI Smart Parcelubi/^AU\d{13}$/——
30Sendlesendle/^(QSS\d{9}|QS\d{6})$/——
31Asendiaasendia/^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=purolator or ?carrier=canadapost. (12-character alphanumeric Purolator PINs like PQR482019376 auto-detect in Pass 2 via /^[A-Z]{3}\d{9}$/, while 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; 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, or 95 (optionally prefixed with 420 + 5-digit destination ZIP, /^(420\d{5})?9[2345]\d{18,24}$/) route to USPS in Pass 1, while 22-digit numbers starting 96 (/^96\d{20}$/) route to FedEx.
  • Consolidator middle-mile scans (?carrier=osm and ?carrier=pitneybowes): Parcels shipped via OSM Worldwide or Pitney Bowes 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 (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=osm or ?carrier=pitneybowes).
  • Asendia: AS…US numbers, USPS domestic induction labels, and destination postal numbers (?carrier=asendia).
  • UniUni: Partner numbers starting GV… or JY… (?carrier=uniuni).
  • GOFO Express: Shipper numbers starting CR… or YT… (?carrier=gofo).
  • 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, 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.

On this page