Blog

Guess the carrier from a tracking number.

format n. 1The shape of a tracking number: its length, its first characters, its check digit. 2A clue to the carrier, not proof.

In this noteShowHide
  1. 1Why the carrier comes first
  2. 2USPS tracking number format
  3. 3The USPS check digit
  4. 4UPS tracking number format
  5. 5FedEx tracking number format
  6. 6DHL tracking number format
  7. 7Numbers that fit two carriers
  8. 8A function that guesses the carrier
  9. 9What a wrong guess costs
  10. 10Sources

A tracking number does not name its carrier, and this API needs the carrier with every request. When the number is all you have, you guess from its shape. This note gives the shapes USPS, UPS, FedEx and DHL publish, the check digits that catch a typo, and a function that returns the carriers worth trying.

1Why the carrier comes first

Every request to POST /v1/track names one carrier: usps, ups, fedex or dhl. The API does not work it out from the number.

If your system made the shipment, it already knows the carrier. Store it next to the number and you never have to guess. Guessing is for numbers that arrive bare: pasted in by a customer, copied from an email, scanned from a label.

A shape is a clue, not proof. Some shapes belong to one carrier. Others are shared by two, and for those the only proof is a lookup.

2USPS tracking number format

USPS calls the barcode on a package label the Intelligent Mail package barcode. Its Publication 199 lays the tracking number out field by field:

FieldDigitsWhat it holds
Channel identifier292 or 93 for commercial mailers, 94 for online postage and postage meters, 95 for Post Office counters and kiosks.
Service type code3The mail class and any extra services.
Source identifier2Only after 94: which postage vendor or system made the label.
Mailer ID6 or 9Who mailed it. A nine-digit ID starts with 9, a six-digit ID with 0 to 8.
Serial number5 to 14The mailer’s own count. It may not repeat within 120 days.
Check digit1Computed from every digit before it.

Together that makes 22, 26 or 30 digits, starting with 92, 93, 94 or 95. Older labels start with 91 and carry a two-digit service code.

USPS prints the number in groups of four. Its tracking page gives these samples:

SampleService
9400 1000 0000 0000 0000 00USPS Tracking
9205 5000 0000 0000 0000 00Priority Mail
9407 3000 0000 0000 0000 00Certified Mail
9270 1000 0000 0000 0000 00Priority Mail Express
EC 000 000 000 USPriority Mail Express International

USPS’s own samples. They show the shape only: none of them passes the check digit.

A scanner reads more than the printed number. The barcode can begin with 420 and the destination ZIP Code, five or nine digits long, which USPS leaves out of the printed text. A scan can therefore give 30 or 34 digits. Drop the 420 and the ZIP Code: the rest is the tracking number.

Mail going abroad uses the 13-character form of the Universal Postal Union: two letters for the service, eight digits, a check digit and the two-letter code of the country it was posted in. Posted in the United States, it ends in US.

3The USPS check digit

The last digit of a USPS number is computed from the others. When it does not match, the number was mistyped, and you can know that without asking USPS.

Publication 199 gives the steps and an example, 9212 3912 3456 7812 3456 70:

  1. Set the last digit aside. It is the check digit, here 0.
  2. From the right, add every second digit, starting with the one next to the digit you set aside: 7 + 5 + 3 + 1 + 7 + 5 + 3 + 1 + 3 + 1 + 9 = 45.
  3. Multiply that by 3: 135.
  4. Add the digits you skipped: 6 + 4 + 2 + 8 + 6 + 4 + 2 + 9 + 2 + 2 = 45.
  5. Add the two results: 180. The check digit is what it takes to reach the next multiple of 10. 180 is one already, so the check digit is 0.

The 420 and the ZIP Code of a scanned barcode take no part in this. Remove them first.

The 13-character numbers have a check digit of their own, the last of the nine digits. Multiply the first eight by 8, 6, 4, 2, 3, 5, 9 and 7, add the results, divide by 11 and subtract the remainder from 11. A result of 10 becomes 0, and 11 becomes 5. The standard’s example: 47312482 gives 9.

4UPS tracking number format

UPS lists four shapes on its tracking support page:

As UPS prints itIn words
1Z99999999999999991Z and 16 more characters
99999999999912 digits
T9999999999T and 10 digits
9999999999 digits

The same page says UPS also tracks freight and delivery-notice numbers of 7 to 20 characters, and UPS Mail Innovations pieces, which can carry a USPS number of 22 to 34 digits.

The last character of a 1Z number is a check digit. UPS’s page does not give the rule; the code below uses the one this API applies. It runs over the fifteen characters between 1Z and the check digit. A digit counts as itself. A letter counts as A = 2, B = 3 and so on up to H = 9, then I = 0, J = 1 and on in the same way. Characters in odd places count once and characters in even places twice. The check digit brings the total to a multiple of 10.

By that rule UPS’s 1Z9999999999999999 would end in 2. Like the USPS samples, it is a shape.

5FedEx tracking number format

A FedEx tracking number is digits only. FedEx’s support pages call it a 12-digit number, and its developer notes also name a 15-digit Ground tracking number.

Those notes mention longer strings as well: a 22-digit Ground barcode and a 34-digit barcode string. FedEx calls them barcodes, not tracking numbers, and the function below does not guess from them.

A door tag, the slip a driver leaves after a missed delivery, has a number of its own: DT and 12 digits. The function does not guess from that either.

FedEx gives no check digit rule on those pages, so for FedEx the function goes by length alone.

6DHL tracking number format

DHL is several businesses under one name, and they do not share one numbering scheme. A DHL Express number is 10 digits: one of its help pages, for every other shape, is titled “Tracking number not 10 digits”.

That page sends the other shapes elsewhere. Numbers that start with 3S, JVGL or VGL, and numbers that start with two letters, go to DHL Parcel. Numbers that start with GM, LX, RX, AU or TH go to DHL eCommerce.

The function guesses dhl for ten digits and for nothing else.

7Numbers that fit two carriers

Two cases cannot be settled by looking.

Twelve digits. FedEx’s tracking number is 12 digits, and UPS lists a 12-digit shape of its own. The function returns both, FedEx first.

A USPS number on another carrier’s label. FedEx’s developer notes say a FedEx Ground Economy shipment still gets a USPS tracking number next to its FedEx number. UPS says its Mail Innovations pieces can carry a USPS number too. Such a number is a real USPS number. The shape says USPS, and so does the function. Nothing in the digits says which company handed the package over.

For everything else the function has no rule: eleven digits, eighteen, twenty, a prefix it does not know. It returns no guess. A wrong guess can cost more than none, as the last section shows.

8A function that guesses the carrier

guessCarrier takes the number as it came, with spaces, hyphens or lowercase letters. It returns the number to send and the carriers to try, most likely first.

// guess-carrier.mjs: which carrier could this tracking number belong to?
// Returns the number to send and the carriers to try, most likely first.
export function guessCarrier(input) {
  const s = String(input).replace(/[^0-9A-Za-z]/g, "").toUpperCase();
  const answer = (carriers, number = s) => ({ number, carriers });

  // UPS: 1Z and 16 more characters, or T and 10 digits.
  if (/^1Z[0-9A-Z]{16}$/.test(s)) return answer(upsCheck(s) ? ["ups"] : []);
  if (/^T\d{10}$/.test(s)) return answer(["ups"]);

  // USPS international: 13 characters that end in US.
  if (/^[A-Z]{2}\d{9}US$/.test(s)) return answer(s10Check(s) ? ["usps"] : []);

  if (!/^\d+$/.test(s)) return answer([]);

  // USPS: starts with 91 to 95 and ends in a check digit. A scanned label
  // puts 420 and a 5- or 9-digit ZIP in front. That part is not the number.
  const tails = s.startsWith("420") ? [s.slice(12), s.slice(8)] : [s];
  const shaped = tails.filter((t) => /^9[1-5]/.test(t) && [22, 26, 30].includes(t.length));
  const usps = shaped.find(mod10);
  if (usps) return answer(["usps"], usps);
  if (shaped.length > 0) return answer([], shaped[0]); // the check digit fails

  // Digits alone: only the length is left to go on.
  const byLength = { 9: ["ups"], 10: ["dhl"], 12: ["fedex", "ups"], 15: ["fedex"] };
  return answer(byLength[s.length] ?? []);
}

// USPS Publication 199, section 4.6: weights 3, 1, 3, 1 ... from the right.
function mod10(digits) {
  let sum = 0;
  for (let i = digits.length - 2, weight = 3; i >= 0; i--, weight = 4 - weight) {
    sum += Number(digits[i]) * weight;
  }
  return (10 - (sum % 10)) % 10 === Number(digits.at(-1));
}

// UPU standard S10, section 5.4: weights 8 6 4 2 3 5 9 7 on the eight digits.
function s10Check(s) {
  const weights = [8, 6, 4, 2, 3, 5, 9, 7];
  const sum = weights.reduce((total, weight, i) => total + weight * Number(s[i + 2]), 0);
  const check = 11 - (sum % 11);
  return (check === 10 ? 0 : check === 11 ? 5 : check) === Number(s[10]);
}

// UPS does not publish this one. Letters count as A=2, B=3 ... H=9, I=0, J=1 ...
function upsCheck(s) {
  let sum = 0;
  for (let i = 2; i < 17; i++) {
    const code = s.charCodeAt(i);
    const value = code < 65 ? code - 48 : (code - 63) % 10;
    sum += i % 2 === 0 ? value : value * 2;
  }
  return (10 - (sum % 10)) % 10 === Number(s[17]);
}

Both versions give the same answer for the same input.

You passCarriersWhy
9212 3912 3456 7812 3456 70uspsUSPS’s example. The check digit is right.
9212 3912 3456 7812 3456 71noneThe same number with the last digit changed.
420, a ZIP Code, then that numberuspsA scanned label. The number comes back without the first part.
12 digitsfedex, upsBoth carriers list the shape.
15 digitsfedexFedEx’s Ground tracking number.
10 digitsdhlA DHL Express number.
9 digitsupsOne of UPS’s four shapes.
11 digitsnoneNone of the four lists that length.

An empty list means no guess: either no rule fits, or a check digit fails. Do not send those.

9What a wrong guess costs

The only proof is a lookup, and the API answers a wrong guess in one of two ways.

CodeHTTPWhat happened
INVALID_TRACKING_NUMBER400Not a number that carrier uses. Nothing was looked up, and nothing is counted.
TRACKING_NOT_FOUND200The shape fits, the carrier was asked, and it has no record. This counts as a lookup.

Letters fail the cheap way: a 1Z number sent as usps is turned away with the 400. A string of digits fits more than one carrier, so a wrong guess there is usually a counted lookup. That is why the order of the candidates matters, and why a number with a failed check digit should not be sent at all.

One gap to know about: the API does not yet take USPS numbers of 26 or 30 digits. It answers them with INVALID_TRACKING_NUMBER even though they are real numbers. The 22-digit kind is looked up.

trackAny tries the candidates in order and stops at the first carrier that has the package. With the function above that is two lookups at most. A 503 was not counted, so the code sends that request again.

// track-any.mjs: Node 18 or later. Tries each candidate until one has data.
import { guessCarrier } from "./guess-carrier.mjs";

const API = "https://api.shipmenttrackingapi.com/v1";
const headers = {
  Authorization: "Bearer YOUR_API_KEY",
  "Content-Type": "application/json",
};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

export async function trackAny(input) {
  const { number, carriers } = guessCarrier(input);
  for (const carrier of carriers) {
    let res;
    for (let attempt = 1; attempt <= 3; attempt++) {
      res = await fetch(`${API}/track`, {
        method: "POST",
        headers,
        body: JSON.stringify({ carrier, trackingNumbers: [number] }),
        signal: AbortSignal.timeout(60_000),
      });
      if (res.status !== 503) break; // a 503 was not counted: send it again
      await sleep(5_000);
    }
    const body = await res.json();
    const result = body.success ? body.data.results[0] : body;
    if (!result.error) return { carrier, ...result };

    // INVALID_TRACKING_NUMBER: not a number that carrier uses. Not counted.
    // TRACKING_NOT_FOUND: the carrier has no record. That lookup counted.
    const { code } = result.error;
    if (code !== "INVALID_TRACKING_NUMBER" && code !== "TRACKING_NOT_FOUND") {
      throw new Error(code);
    }
  }
  return null; // no guess, or no carrier had it
}
The lookup needs a key. In our checks one request took 9 to 39 seconds.Get a free key

UPS, FedEx and DHL take one number per request, so each guess is a request of its own. USPS numbers that pass the check can go together, up to 35 in one request: see Track 35 USPS packages in one request.

Caution

A TRACKING_NOT_FOUND counts whether the guess was right or wrong. A free key has 1,000 lookups a month, so guess only when the carrier is truly unknown.

10Sources

The formats above come from the carriers’ own pages, read on Nov 12, 2026. Carriers change them without notice.

ShipmentTrackingAPI is not affiliated with any of these carriers, and none of them has reviewed this note.

See alsoAll notes
StartAbout a minute

Sign in with Google. Your key is ready in seconds.