In this noteShowHide
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:
| Field | Digits | What it holds |
|---|---|---|
| Channel identifier | 2 | 92 or 93 for commercial mailers, 94 for online postage and postage meters, 95 for Post Office counters and kiosks. |
| Service type code | 3 | The mail class and any extra services. |
| Source identifier | 2 | Only after 94: which postage vendor or system made the label. |
| Mailer ID | 6 or 9 | Who mailed it. A nine-digit ID starts with 9, a six-digit ID with 0 to 8. |
| Serial number | 5 to 14 | The mailer’s own count. It may not repeat within 120 days. |
| Check digit | 1 | Computed 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:
| Sample | Service |
|---|---|
| 9400 1000 0000 0000 0000 00 | USPS Tracking |
| 9205 5000 0000 0000 0000 00 | Priority Mail |
| 9407 3000 0000 0000 0000 00 | Certified Mail |
| 9270 1000 0000 0000 0000 00 | Priority Mail Express |
| EC 000 000 000 US | Priority 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:
- Set the last digit aside. It is the check digit, here 0.
- 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.
- Multiply that by 3: 135.
- Add the digits you skipped: 6 + 4 + 2 + 8 + 6 + 4 + 2 + 9 + 2 + 2 = 45.
- 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 it | In words |
|---|---|
| 1Z9999999999999999 | 1Z and 16 more characters |
| 999999999999 | 12 digits |
| T9999999999 | T and 10 digits |
| 999999999 | 9 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]);
} # guess_carrier.py: which carrier could this tracking number belong to?
import re
def guess_carrier(text):
"""Return the number to send and the carriers to try, most likely first."""
s = re.sub(r"[^0-9A-Za-z]", "", str(text)).upper()
def answer(carriers, number=s):
return {"number": number, "carriers": carriers}
# UPS: 1Z and 16 more characters, or T and 10 digits.
if re.fullmatch(r"1Z[0-9A-Z]{16}", s):
return answer(["ups"] if ups_check(s) else [])
if re.fullmatch(r"T[0-9]{10}", s):
return answer(["ups"])
# USPS international: 13 characters that end in US.
if re.fullmatch(r"[A-Z]{2}[0-9]{9}US", s):
return answer(["usps"] if s10_check(s) else [])
if not re.fullmatch(r"[0-9]+", 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.
tails = [s[12:], s[8:]] if s.startswith("420") else [s]
shaped = [t for t in tails if re.match(r"9[1-5]", t) and len(t) in (22, 26, 30)]
usps = next((t for t in shaped if mod10(t)), None)
if usps:
return answer(["usps"], usps)
if shaped:
return answer([], shaped[0]) # the check digit fails
# Digits alone: only the length is left to go on.
by_length = {9: ["ups"], 10: ["dhl"], 12: ["fedex", "ups"], 15: ["fedex"]}
return answer(by_length.get(len(s), []))
def mod10(digits):
"""USPS Publication 199, section 4.6: weights 3, 1, 3, 1 ... from the right."""
total = sum(int(d) * (3 if i % 2 == 0 else 1) for i, d in enumerate(reversed(digits[:-1])))
return (10 - total % 10) % 10 == int(digits[-1])
def s10_check(s):
"""UPU standard S10, section 5.4: weights 8 6 4 2 3 5 9 7 on the eight digits."""
total = sum(w * int(d) for w, d in zip((8, 6, 4, 2, 3, 5, 9, 7), s[2:10]))
check = 11 - total % 11
return {10: 0, 11: 5}.get(check, check) == int(s[10])
def ups_check(s):
"""UPS does not publish this one. Letters count as A=2, B=3 ... H=9, I=0, J=1 ..."""
total = 0
for i, c in enumerate(s[2:17]):
value = int(c) if c.isdigit() else (ord(c) - 63) % 10
total += value if i % 2 == 0 else value * 2
return s[17].isdigit() and (10 - total % 10) % 10 == int(s[17]) Both versions give the same answer for the same input.
| You pass | Carriers | Why |
|---|---|---|
| 9212 3912 3456 7812 3456 70 | usps | USPS’s example. The check digit is right. |
| 9212 3912 3456 7812 3456 71 | none | The same number with the last digit changed. |
| 420, a ZIP Code, then that number | usps | A scanned label. The number comes back without the first part. |
| 12 digits | fedex, ups | Both carriers list the shape. |
| 15 digits | fedex | FedEx’s Ground tracking number. |
| 10 digits | dhl | A DHL Express number. |
| 9 digits | ups | One of UPS’s four shapes. |
| 11 digits | none | None 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.
| Code | HTTP | What happened |
|---|---|---|
| INVALID_TRACKING_NUMBER | 400 | Not a number that carrier uses. Nothing was looked up, and nothing is counted. |
| TRACKING_NOT_FOUND | 200 | The 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
} # track_any.py: Python 3, with the requests package.
import time
import requests
from guess_carrier import guess_carrier
API = "https://api.shipmenttrackingapi.com/v1"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
def track_any(text):
"""Try each candidate until one has tracking data."""
guess = guess_carrier(text)
for carrier in guess["carriers"]:
for attempt in range(3):
res = requests.post(
f"{API}/track",
headers=HEADERS,
json={"carrier": carrier, "trackingNumbers": [guess["number"]]},
timeout=60,
)
if res.status_code != 503: # a 503 was not counted: send it again
break
time.sleep(5)
body = res.json()
result = body["data"]["results"][0] if body["success"] else body
if "error" not in result:
return {"carrier": carrier, **result}
# INVALID_TRACKING_NUMBER: not a number that carrier uses. Not counted.
# TRACKING_NOT_FOUND: the carrier has no record. That lookup counted.
code = result["error"]["code"]
if code not in ("INVALID_TRACKING_NUMBER", "TRACKING_NOT_FOUND"):
raise RuntimeError(code)
return None # no guess, or no carrier had it 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.
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.
- USPS, Publication 199, version 35, July 2, 2026: sections 4.1 to 4.6 and Appendix J.
- USPS, USPS Tracking: “What does my tracking number look like?”
- Universal Postal Union, standard S10, version 12: section 5.
- UPS, Tracking Support: “What do tracking numbers look like?”
- FedEx, What is an air waybill?, Door Tag Help, and the developer announcement on FedEx Ground Economy, April 2022.
- DHL Express, Tracking number not 10 digits, as shown for Andorra.
ShipmentTrackingAPI is not affiliated with any of these carriers, and none of them has reviewed this note.