One thesis: GTIN-8, GTIN-12, GTIN-13 and GTIN-14 are not four identifiers — they are one identifier at four encodings, and systems that store them as strings of different lengths manufacture join failures out of thin air. The fix is a one-line law: canonicalize to the zero-padded GTIN-14 everywhere, and keep the source form as metadata.
If you are the engineer who owns the parsing layer of a supply-chain product, you have seen the ticket: "item not found — invalid barcode." The item exists. The barcode is valid. What failed is that one system stored 614141000036 (twelve digits, off a UPC-A), another stored 00614141000036 (fourteen, off a case file), and the join compared them as unequal strings. That is not a data problem. It is a missing canonicalization law.
The grammar, and where each length lives
| Form | Digits | Where it comes from |
|---|---|---|
| GTIN-12 | 12 | UPC-A at US retail POS |
| GTIN-13 | 13 | EAN-13 outside North America; catalog systems |
| GTIN-14 | 14 | ITF-14 case marks, GS1-128 logistics labels, AI (01) |
| GTIN-8 | 8 | EAN-8 — a separate short allocation for small packs |
Structure is the same throughout: GS1 company prefix, item reference, check digit — with GTIN-14 adding a leading indicator digit. Canonicalization law: left-pad every form with zeros to 14 digits. A GTIN-12 becomes 00 + twelve; a GTIN-13 becomes 0 + thirteen; GTIN-8 pads with six zeros. One identity, one key, every join closes.
One caution the padding rule must never blur: a nonzero leading indicator digit is not padding. 00614141000036 and 10614141000036 are different GTINs — the indicator digit changes the identity (typically to a case-level packaging of the same product) and the check digit is recomputed. Padding adds zeros to reach 14; it never invents an indicator.
The check digit, worked from the specification
The algorithm is GS1 General Specifications §7.9 — the full text openly served at ref.gs1.org, royalty-free to implement. For the GTIN-12 614141000036: weight the first eleven digits alternately 3 and 1, left to right:
6·3 + 1·1 + 4·3 + 1·1 + 4·3 + 1·1 + 0·3 + 0·1 + 0·3 + 0·1 + 3·3
= 18 + 1 + 12 + 1 + 12 + 1 + 0 + 0 + 0 + 0 + 9
= 54
Check digit = (10 − 54 mod 10) mod 10 = 6. The twelfth digit is 6. Valid.
Now the fact that makes canonicalization safe, and it is a lovely one: the weights are anchored at the right-hand end, alternating 3-1 outward from the digit beside the check digit. Leading zeros therefore multiply into the sum as zero and shift nothing. The same eleven meaningful digits produce the same check digit at every length:
$ barcoding verify 614141000036 --json
{ "scheme": "gs1.gtin", "input": "614141000036", "carrier": "gtin-12",
"canonical": "00614141000036", "check": "mod10 pass" }
$ barcoding verify 0614141000036 --json
{ "scheme": "gs1.gtin", "input": "0614141000036", "carrier": "gtin-13",
"canonical": "00614141000036", "check": "mod10 pass" }
$ barcoding verify 00614141000036 --json
{ "scheme": "gs1.gtin", "input": "00614141000036", "carrier": "gtin-14",
"canonical": "00614141000036", "check": "mod10 pass" }
Three inputs, three source forms preserved as metadata, one canonical identity. Every verification runs pure and local against pinned check-digit vectors — barcoding pins prints the digest of the vector file, so the algorithm your build ran is a fact you can cite, not a routine somebody transcribed in 2019. The full grammar — prefix structure, allocation rules, symbology bindings — is the registry page at /schemes/gtin.
The bug taxonomy, reread as canonicalization
Walk your backlog with the law in hand and watch the tickets reclassify:
- The Excel amputation. A spreadsheet ate the leading zero,
00036000291452became36000291452, and eleven digits validate as nothing. The check digit still catches it — eleven digits is not a form — but only if you verify before you store. - The cross-system join miss. POS stores 12, item master stores 13, WMS stores 14. All three "have the barcode"; no two agree. Canonical GTIN-14 as the join key ends the category.
- The truncation-as-conversion myth. Converting GTIN-13 to GTIN-12 by dropping the leading zero works only for North-American prefixes that started life as UPC-A. Applied to a real EAN-13, it produces a different, possibly valid, wrong identity — the worst failure class, because nothing alarms.
- The checksum-equals-legitimacy conflation.
verifychecks two different things and says so separately: the mod-10 arithmetic (is this a well-formed GTIN?) and prefix legitimacy against the pinned GS1 prefix ranges (is this prefix actually allocated, and to which issuing MO?). A checksum-valid number with an unallocated prefix is a well-formed fiction — and a parser that reports only "valid" cannot tell you which claim it is making.
None of these is exotic. Together they are most of what "invalid barcode" means in a production queue — and every one of them is prevented at the door by the same three moves: verify, canonicalize to 14, keep the source form. The symbology post makes the same argument one layer down: carrier and identity are separate facts, and systems that model them separately stop generating this ticket class.
The maintenance argument is the honest close. The algorithm is a sprint; the tables move forever — prefix ranges get allocated, grammar versions ratify. Embedding a layer that pins those artifacts and prints its digests beats re-transcribing the standard per project, which is the same build-vs-maintain arithmetic the family's event layer makes at epcis.dev. Fourteen digits, verified and canonical, is a small law. It is also the difference between an identifier and a string.
Keys open the network half of the verb set. Get an API key — say what arrives at your door, and the provisioning order follows the list.
We answer in writing. We take at most five conversations a month, only when you ask for one, and only after you already have the written read.