🍱 Lunchbox Hands

barcode

The UPC/EAN Check Digit Algorithm: Mod 10, Worked by Hand

How the GS1 mod-10 check digit actually works — the 3/1 weights anchored at the right end, a full digit-by-digit worked example, why UPC-A and EAN-13 share one algorithm, and the exact transposition errors mod 10 fails to catch.

The last digit of every UPC or EAN barcode isn’t part of the product number. Nobody chose it, and it carries no information about the product at all. It’s computed from the other digits — an alternating ×3/×1 weighted sum taken mod 10, anchored at the right end of the number — and that one design choice explains everything: why a scanner can reject a misprint instantly, why UPC-A and EAN-13 are secretly the same code, and exactly which typos the whole scheme is blind to.

The algorithm fits on an index card, and GS1 (the organization that issues these numbers) publishes it openly. Here it is, worked all the way through.

The algorithm in three steps

GS1’s manual calculation procedure, from its published check-digit fact sheet, applies to every ID length it issues — GTIN-8 (EAN-8), GTIN-12 (UPC-A), GTIN-13 (EAN-13), GTIN-14 (ITF-14), even 18-digit SSCC pallet codes:

  1. Weight each digit ×3 or ×1, alternating — anchored at the right. The digit immediately to the left of the check-digit position always gets ×3, its neighbor ×1, and so on leftward.
  2. Sum the products.
  3. Subtract the sum from the next-highest multiple of ten. The difference is the check digit. (If the sum already lands on a multiple of ten, the check digit is 0.)

Step 3 is just a friendly phrasing of (10 − sum mod 10) mod 10 — the check digit is whatever makes the grand total, check digit included, divisible by ten. That’s the entire verification rule: weight every digit of the full number, add, and the result must end in 0.

Worked example, digit by digit

GS1’s own example: find the check digit for the 12-digit EAN-13 body 931234567890. Twelve digits means the rightmost body digit gets ×3, so reading left to right the weights run 1, 3, 1, 3, …

Positionn1n2n3n4n5n6n7n8n9n10n11n12
Digit931234567890
Weight×1×3×1×3×1×3×1×3×1×3×1×3
Product991631251872490

Sum: 9 + 9 + 1 + 6 + 3 + 12 + 5 + 18 + 7 + 24 + 9 + 0 = 103. The next multiple of ten is 110, and 110 − 103 = 7. Full barcode: 9312345678907.

To verify a scanned number you run the same sum over all thirteen digits (the check digit itself gets weight ×1, since it sits one to the right of the ×3 position): 103 + 7 = 110. Ends in zero — valid.

UPC-A is EAN-13 with a leading zero

A UPC-A is 12 digits; an EAN-13 is 13. But they aren’t two systems. In GS1’s numbering, a UPC-A is a GTIN-12, and prefixing a 0 turns it into the equivalent GTIN-13 — same product, same check digit, scannable by the same hardware.

The reason the check digit survives that transformation is the right-anchoring in step 1. Because the alternating weights count from the right end, every existing digit keeps its weight when you prepend a zero — and the new leading 0 contributes nothing to the sum no matter what it’s multiplied by. Try it: the UPC-A body 03600029145 has weighted sum 58 (weights 3, 1, 3, … this time, because an 11-digit body starts on ×3), so its check digit is 60 − 58 = 2, giving 036000291452. Prepend a zero and compute the 13-digit version, 0036000291452: same sum, same 2.

This is also why EAN-8, ITF-14, and the rest share one algorithm — the weight pattern is defined from the check digit outward, so the length of the number never matters.

What mod 10 catches — and what it provably misses

The scheme’s real job is catching human error, and it’s worth being precise about the coverage.

Every single-digit error is caught. Change any one digit and the sum changes by either 1× or 3× the difference. Since neither 1 nor 3 shares a factor with 10, no wrong digit can produce the same last digit of the sum. This is guaranteed, not probabilistic.

Most adjacent transpositions are caught — but not all. Swap two neighboring digits a and b and their weights (3 and 1) swap between them, so the sum changes by (3a + b) − (a + 3b) = 2(a − b). That’s detectable unless 2(a − b) is a multiple of 10 — which happens exactly when the two digits differ by 5. So the five pairs 0↔5, 1↔6, 2↔7, 3↔8, 4↔9 slip through when swapped as neighbors.

You can watch it fail. Take the EAN-13 body 400000000027: weighted sum 4 + 2 + (7×3) = 27, check digit 3. Now fat-finger the 27 into 72 — body 400000000072: sum 4 + 7 + (2×3) = 17, check digit… 3 again. Both 4000000000273 and 4000000000723 are perfectly valid EAN-13s, and no scanner will flag the swap.

Compare a swap the scheme does catch — 12 versus 21 in the same slot:

Body ends in…Weighted sumCheck digitCaught?
…124 + 1 + 6 = 119
…21 (swapped)4 + 2 + 3 = 91✓ different digit
…274 + 2 + 21 = 273
…72 (swapped)4 + 7 + 6 = 173✗ identical digit

One decimal digit of redundancy can’t do better than reject 90% of random garbage, and this particular weighting spends its power on the errors humans actually make: single wrong digits (all caught) and neighbor swaps (four out of every five digit-pairs caught).

”Rejected” vs. “not found” are different failures

This distinction confuses people at self-checkouts and in inventory systems alike:

  • Check digit mismatch — the number is structurally wrong. The scanner’s decoder (or your order-entry form) recomputes the mod-10 sum and it doesn’t end in zero. Something was misprinted, misread, or mistyped. The number never reaches a database.
  • Valid check digit, unknown product — the arithmetic passes, so the code is well-formed, but no catalog has a row for it. That’s a lookup failure, not a barcode failure.

The check digit validates structure, never existence. 4000000000273 from our counterexample above is mathematically flawless and almost certainly identifies nothing. Passing the mod-10 test means “this could be a real GTIN,” the same way a Luhn-valid number could be a real credit card.

Cousins: ISBN-13 and Luhn

Two other check systems developers bump into constantly:

  • ISBN-13 is literally the same algorithm. A modern ISBN is an EAN-13 in the 978/979 prefix range, so its final digit is the identical ×3/×1 mod-10 computation — which is how a book’s ISBN and its barcode can be one number.
  • Luhn (credit cards) is the same idea with different weights. Instead of ×3, Luhn doubles every second digit and adds the digits of each product (so 7 doubles to 14, contributing 1 + 4 = 5). That digit-summing step is Luhn’s fix for the differ-by-5 blind spot — but it buys its own tiny hole, missing only the 09↔90 swap. We dug into Luhn’s limits — including why a 1-in-10 false-positive rate makes it a terrible PII detector — in why regex PII redaction leaks.

Compute it in eight lines

The right-anchored weighting makes the code shorter than the explanation:

function gs1CheckDigit(body) {
  // body: the digits WITHOUT the check digit, any GS1 length
  let sum = 0;
  for (let i = 0; i < body.length; i++) {
    // rightmost body digit gets ×3, alternating leftward
    const weight = (body.length - 1 - i) % 2 === 0 ? 3 : 1;
    sum += Number(body[i]) * weight;
  }
  return (10 - (sum % 10)) % 10;
}

gs1CheckDigit('931234567890'); // 7 → EAN-13 9312345678907
gs1CheckDigit('03600029145');  // 2 → UPC-A 036000291452
gs1CheckDigit('4000000000');   // works for any length — weights anchor right

Note what anchors where: iterating left to right, the weight depends on the distance from the end of the string, not the index. Write i % 2 instead and the function silently breaks for odd-length bodies like UPC-A’s eleven digits.

Try it on a real barcode

Our barcode generator renders EAN-13, EAN-8, UPC-A, and ITF-14 (plus non-GTIN symbologies like Code128, Code39, MSI, and Codabar) as downloadable SVG, entirely in your browser. It uses JsBarcode under the hood, which handles the check digit exactly the way this post describes:

  • Give it a body without the check digit — 12 digits for EAN-13, 11 for UPC-A, 7 for EAN-8, 13 for ITF-14 — and it computes and appends the check digit for you.
  • Give it the full-length number and it validates instead: a wrong final digit gets flagged as invalid rather than rendered.

Which makes it a hands-on lab for everything above. Type 931234567890 as EAN-13 and watch the 7 appear in the rendered text. Type 9312345678901 and it’s rejected. Then try both halves of the counterexample — 4000000000273 and 4000000000723 — and confirm the algorithm happily blesses the transposed version, exactly as the arithmetic said it would.

The short version

  • Check digit = whatever makes the ×3/×1-weighted sum of all digits end in 0; weights alternate starting with ×3 immediately left of the check digit.
  • Weights anchor at the right end — that’s why one algorithm covers EAN-8 through SSCC, and why UPC-A is just EAN-13 with a leading 0.
  • Catches all single-digit errors and all adjacent swaps except pairs differing by 5 (05, 16, 27, 38, 49).
  • A passing check digit means well-formed, not real: mismatch = misprint or typo; valid-but-unknown = database miss.
  • ISBN-13 is the same computation; Luhn is the doubled-digit cousin with a different blind spot.