Card Number Formats Explained: Hex, Decimal and Byte Order

2026-09-30 · 1139 words

A card holds a few bytes of opaque identifier. The number a reader shows you is a projection of those bytes — a base and a byte order chosen by whoever wrote the firmware — and nothing more. That is why the same credential can read as 04A2243A on the installer's laptop, 04:A2:24:3A on the controller, 77734970 in one database and 975479300 in another. Four correct answers, zero agreement.

Once the formats are seen as bytes plus two decisions — which base, and which end of the byte string is byte zero — the conversions stop looking like magic and start looking like a checklist.

A card UID is bytes; the number is a projection

125 kHz proximity cards and Mifare Classic carry a 4-byte UID. DESFire, Ultralight and many ISO 14443-4 cards carry 7 bytes; a handful carry 10. Those bytes are an identifier, not a quantity: there is no "true" decimal value for a UID, only the value some system computed from it.

The length of the number is therefore evidence. A decimal number above 4,294,967,295 (2^32 − 1) cannot have come from 4 bytes. A number above roughly 72,057,594,037,927,935 (2^56 − 1) cannot have come from 7 bytes. Those two bounds usually narrow the card family down before anything else has been looked at — and they are the reason a "16-digit decimal card number" is a contradiction unless the producer is printing something other than a raw UID, such as a printed credential number from a separate database column.

The forms, and how they relate

Take one card whose UID is 04 A2 24 3A:

bytes on the card      04 A2 24 3A
big-endian hex         04A2243A            ← the standard hex print
colon-grouped hex      04:A2:24:3A         ← the same bytes, readable
decimal (big-endian)   77,734,970          ← 04A2243A as a 32-bit integer
little-endian hex      3A24A204            ← the byte string reversed
reversed decimal       975,479,300         ← the reversal, as an integer

Read that list again, because it contains the whole problem: 77,734,970 and 975,479,300 are the same card. A system given the wrong one will accept nothing at all, or worse, accept a number that belongs to a different credential.

The two directions that appear in the wild are:

The mapping is a byte reversal, not a digit reversal. Reversing 975,479,300 as digits gives 003,974,579, which is not a card at all; reversing the bytes gives 3A24A204, and reversing that gives back 04A2243A. A converter that reverses decimal digits has misunderstood the format, and the numbers it produces look entirely plausible.

Where the conversion invents a number

Three failure modes account for most "this card works on one door and not the other" reports.

Thousands separators. Copy a column out of Excel and you get the display text: 2,864,434,397, commas included. Split naively on commas and one card becomes two cells, 2 and 864,434,397 — and a converter that reports success has just produced two card numbers that do not exist. The correct reading is that 2,864,434,397 is one value (it is AABBCCDD in hex), and the correct behaviour for the ambiguous case is to refuse the input rather than guess. The judgement call is whether a comma is a thousands separator or a column delimiter; it needs a test, not a comment.

Leading zeros. A byte of 0x0A is 10 in decimal and 0A in hex — the leading zero survives in hex and disappears in decimal. A 4-byte UID whose first byte is 0x00 is legal, and it becomes indistinguishable from a 3-byte number the moment it is stored as an integer. Store UIDs as byte strings or as fixed-width hex, never as integers, or the first byte you lose will be the one that made the card unique.

Precision. A 7-byte UID does not always fit in a JavaScript Number. Number.MAX_SAFE_INTEGER is 9,007,199,254,740,992 (2^53), and any 7-byte UID whose first byte is 0x20 or above exceeds it. NXP-style UIDs that begin with 0x04 top out at 1,407,374,883,553,279 and stay inside the safe range by luck — a property of that one leading byte, not something to rely on. Parse the hex digits with BigInt when the field is 7 bytes wide:

const s = '461B6C44F53981';

parseInt(s, 16);                       // 19733400197085570  ← wrong
BigInt('0x' + s).toString(10);         // 19733400197085569  ← the card

The two values differ in the last digit, and the wrong one is not an error: it is a valid number that will enroll happily and never match the card again. That is the characteristic failure of this whole subject — a plausible wrong answer instead of a failure.

Parsing a column instead of a value

Real work arrives as a column pasted out of a spreadsheet, not as one clean string, and the column is rarely uniform: some rows decimal as the reader printed them, some hex with colons, some reversed. Guessing a format per cell is how half a table ends up wrong while the tool reports no failures at all.

What is needed is a converter that says which row it rejected and why. "Row 14: five colon-separated groups is not a 4-byte or 7-byte UID" is actionable; "invalid input" for 200 rows is not. The card number converter works that way by design: paste a column in any of the forms above and it shows every form of each value, refuses rows it cannot read unambiguously, and exports the table as CSV for whatever system needs it next. It reads the bytes and prints projections — it never rounds a projection back into bytes and calls the result the card.

When two systems disagree, check in this order

  1. Length. 4 bytes, 7 bytes or 10? Half the hypotheses are eliminated by this alone.
  2. Byte order. Reverse the bytes and compare again. If the reversed value matches, the disagreement is found and it is not a hardware fault.
  3. Leading zeros. Compare the hex forms rather than the decimal ones; if one side is a byte short, that is the difference.
  4. Base. 10000000 could be decimal, or hex, or a number that happens to look round. Compare as fixed-width hex before concluding that two systems hold different cards.
  5. Frame, not UID. If the number came out of a Wiegand frame rather than being a raw UID, the split between facility code and card number is involved — see Wiegand 26 vs 34, where one credential legitimately becomes two different numbers.

While debugging, keep hex and Base64 apart. Hex is one character per nibble and is a representation of the bytes; Base64 is a different encoding with a four-for-three expansion, and a string that looks hexadecimal is not automatically hex — the comparison of hex, Base32, Base64 and Ascii85 covers how to tell them apart from the string itself.

The habit that prevents most of this: normalise on the way in, keep the bytes, and print only the form the receiving system asked for. Every extra round trip through a decimal integer is another chance to reverse something that was already reversed.

Related reading

Try the Base64 to Image converter →