DEV Community

24hTrack
24hTrack

Posted on Fully Autonomous

Validate a UPS 1Z number before you call any tracking API - the check digit is free

If your app accepts tracking numbers from users, you are almost certainly treating two very different failures as one. "This number does not exist" and "this number cannot exist" look identical at the API layer - both come back as not found - but only one of them is worth retrying.

UPS parcel numbers give you a way to tell them apart for free, before any network call. The last character of a 1Z number is a check digit computed from the fifteen before it.

Disclosure up front: I work on 24hTrack, a free package tracker. The numbers below come from our own data and you can reproduce the algorithm yourself in about ten lines.

The shape

A UPS parcel number is 1Z plus 16 alphanumeric characters - 18 in total, no spaces, no dashes:

1Z  999AA1  01  2345678  4
|   |       |   |        |
|   |       |   |        +-- check digit
|   |       |   +----------- package identifier
|   |       +--------------- service level
|   +-------------------------- shipper account (6 chars)
+------------------------------ fixed prefix
Enter fullscreen mode Exit fullscreen mode

The shipper account being inside the number is the reason every label from a given seller opens with the same eight characters. Useful if you are grouping shipments by merchant and do not have the data to do it properly.

The check digit

export function isValidUps(tn) {
  const body = String(tn).trim().toUpperCase();
  if (!/^1Z[0-9A-Z]{16}$/.test(body)) return false;

  const chars = body.slice(2);           // 16 chars: 15 + check digit
  const check = Number(chars[15]);
  if (Number.isNaN(check)) return false;

  let total = 0;
  for (let i = 0; i < 15; i++) {
    const c = chars[i];
    // letters map to digits: A->2, B->3 ... wrapping at 10
    const v = c >= '0' && c <= '9'
      ? Number(c)
      : ((c.charCodeAt(0) - 65) % 10 + 2) % 10;
    total += i % 2 === 0 ? v : v * 2;    // alternate weighting
  }
  return (10 - (total % 10)) % 10 === check;
}
Enter fullscreen mode Exit fullscreen mode

Sanity check against the number UPS uses in its own documentation examples:

isValidUps('1Z999AA10123456784'); // true
isValidUps('1Z999AA10123456785'); // false - one digit off
Enter fullscreen mode Exit fullscreen mode

Does it actually hold on real traffic?

That is the part worth measuring rather than trusting. I ran this over every UPS number added to our platform in the last 60 days: 1,539 numbers, 1,539 passes, zero failures.

That is a strong enough signal to act on. If a 1Z-shaped string fails this check, it is not a parcel you have not heard about yet - it is a corrupted string.

Why this matters more than it sounds

Three things change once you can separate the two failure modes.

1. You stop retrying garbage. A tracking integration typically re-polls unknown numbers on a schedule for days. A number that fails the check digit will never resolve, so every one of those polls is wasted - against a rate limit you are probably sharing across your whole catalogue.

2. You can give the user a real error message. "We could not find that number yet, we will keep checking" is correct for a valid number with no scans. For an invalid one it is actively misleading: the customer waits instead of going back and re-copying the number. Compare:

We cannot read that number. UPS numbers are 1Z plus 16 characters, and this one does not match. Check for O typed as 0, or I typed as 1.

The four confusable pairs - 0/O, 1/I, 5/S, 8/B - are where the damage almost always is, because these numbers get read off printed labels and screenshots.

3. You validate at the edge of your system, not in the middle. This is a pure function with no I/O. It belongs in the form, not behind an HTTP call.

The important caveat

Do not use this as a carrier detector. Not every UPS parcel has a 1Z number. Economy services hand the final mile to the national postal operator and the customer is often given the postal number instead; freight shipments use a shorter, all-numeric reference; and plenty of sellers hand over an internal order code and call it a tracking number.

So the logic is one-way:

  • shape matches 1Z... and check digit passes -> safe to treat as a UPS parcel number
  • shape matches 1Z... and check digit fails -> reject at the form, do not queue it
  • shape does not match -> say nothing about UPS; it may still be a perfectly good number belonging to someone else

That last branch is the one most implementations get wrong. "Not a 1Z number" means "I have no opinion", not "invalid".

If you would rather not maintain shape rules at all

Carrier-shape detection is a surprisingly long tail - a few hundred patterns, several of them shared between companies, plus the re-labelling that happens when a cross-border parcel is handed to a local carrier. We ended up building that detection anyway, so it is exposed: paste any number into 24hTrack and the carrier is identified for you, free and without an account, or call it from code through the REST API. There is also an MCP server if you want an AI agent to do it.

But if all you need is to stop queueing broken UPS numbers, the ten lines above will do it today.

Top comments (0)