
One Code Per Item: Serializing QR Codes Properly
Four thousand valve bodies ship with a laser-marked code on each. Three months later the warranty page behind them has to move, and it moves in ten minutes, because every code was dynamic. Then a field engineer asks whether the code on a returned unit matches that unit's build record, and the answer takes two days, because the serial is not in the code. The code carries a random token, and the token-to-serial mapping lived in a spreadsheet somebody re-sorted and regenerated.
Serializing codes is not difficult. Deciding where the serial lives, and what it is allowed to mean, determines whether you can answer questions three years later.
Three places a serial can live
In the payload, as the whole payload. The code encodes SN-0004821 and nothing else. A phone camera shows the string and offers nothing to do with it, while a scanner app wired into your system looks it up. This is right for closed loops: a line-side reader, a warehouse gun, a service tablet running your app. It is the smallest possible payload and needs no network.
In the path of a URL. The code encodes https://ex.co/i/0004821. Every item gets its own address, that address resolves to a per-item page, and anybody with a phone gets something useful. This is the answer whenever a human outside your organization will scan it.
As a parameter on a shared base. https://ex.co/p?sn=0004821. Close to the path version and worse in one way: a long shared prefix means a thousand codes differ only in the tail, so a mis-sorted roll of labels is invisible to the eye and nearly invisible to a spot check.
If the item is a retail product with a GTIN there is a fourth option: carry the serial inside a GS1 Digital Link alongside the product number using the standard identifiers. Where codes will ever be read by a retail or regulated supply chain rather than only by you, start there rather than inventing a scheme.
What the printed code can never change
A dynamic code lets you change where the code goes. It does not let you change what the code says. The string inside the symbol was fixed the moment the mark was made, and it stays fixed for the life of the object.
So the serial in the printed code must be an identifier and nothing more. Do not encode the owner, the location, the configuration, the distributor, the price band, the firmware revision or the intended market. Every one of those changes, and when it changes you have four thousand marked objects asserting something false. Put an opaque identifier in the code and let the record behind it hold every mutable fact, which is the discipline asset tags need for the same reason. Two facts are safe to encode, because they never change for that object: what it is, and which one it is.
What a serial costs in module width
A serial is characters, characters are capacity, and capacity is version. Walk that chain before fixing the label size.
URLs land in byte mode, because they carry lowercase letters and punctuation the denser alphanumeric mode does not include, so budget eight bits per character; the mode table explains why. At the highest error correction level, version 3 holds 24 bytes, version 4 holds 34, version 5 holds 44 and version 6 holds 58. At a lower level the same payload fits a smaller grid, and the trade is damage tolerance.
Take a base of https://ex.co/i/, sixteen characters. A six-digit serial brings the payload to 22 characters, fitting version 3: a 29 by 29 grid, 37 module widths once the four-module quiet zone ISO asks for is added on each side, which on a 20 mm label is 0.54 mm per module. Now use a UUID instead. The payload reaches 52 characters and needs version 6: 41 by 41, so 49 module widths with the margin, and 0.41 mm per module on the same label. You have given up a quarter of your module width for an identifier nobody will ever read aloud.
So the public identifier and the internal identifier should not be the same string. Keep a UUID or a database key internally and mint a short opaque public code for the label. Eight characters of base 36 gives about 2.8 trillion values, beyond any production run, and costs one version rather than three.
Sequential serials give away more than you think
Serials that count up are convenient and they leak. A competitor who buys two of your units a month apart reads both and knows roughly how many you made in between, which is production volume handed over on the label. And anybody who scans one code and increments the last digit lands on a valid page for an item they have never seen.
Two fixes, solving different problems. Mint non-sequential public identifiers drawn from a space large enough that guessing fails, and keep the sequential internal ID private. Or keep a readable sequential serial and append a short keyed token derived from it, so a guessed serial without the matching token resolves to nothing.
Both stop guessing. Neither stops copying, because a photographed code is byte-identical to the original and no symbol can prove it is the one that was printed. The only thing that detects duplication is behaviour over time: the same identifier scanned in two countries in one week, or a first scan arriving after the record says the item was activated. That is the real content of product authentication, and it lives in your back end rather than in the mark.
Check digits, and which one
Somebody will eventually read the serial off the label with a thumb over part of it and type it into a form. A check digit is one extra character that catches most of what they get wrong, and the algorithms differ in what exactly.
- Luhn, mod 10. The familiar one from card numbers. Catches every single-digit substitution and every transposition of adjacent digits except 09 swapped for 90.
- The GS1 mod 10 check digit. Alternating weights of 3 and 1, required inside a GS1 scheme. Catches all single-digit errors but not all transpositions.
- Damm or Verhoeff. Both catch every single-digit error and every adjacent transposition with no exceptions, for a little more implementation and no downside.
- ISO 7064 mod 97-10. Two check characters, the scheme behind IBAN. Worth it when the serial is long and a wrong lookup is expensive.
Print the serial next to the code, with its check digit, in a typeface that distinguishes zero from capital O. Then a code that will not scan is still a serial somebody can read out over the phone.
Proofing a run where every label is different
A thousand identical labels can be proofed by looking at one. A thousand serialized labels cannot be proofed by looking at any, because they all look the same and the differences sit in the middle of a string.
Decode rather than inspect, and pick the sample deliberately: the first label, the last, and a block either side of every roll change or sheet break. Splices and off-by-one feeds are where serialized runs go wrong, and they go wrong in contiguous blocks rather than at random, so random sampling finds them late. Compare each decoded string against its source row with a script, not by eye. The full routine, including what a spreadsheet silently does to a twelve-digit serial, is in CSV workflows for QR codes, and the identifier scheme should follow whatever naming convention the rest of your codes use.
Then freeze that file and archive it with the date, the quantity and where the material went. Three years from now the only question anybody asks about a serialized code is which object it was on.


