How to Verify an EAN-13 Barcode Scan (Check Digit Formula)

An EAN-13 scan is correct when its last digit matches the check digit of the first 12. Formula, JS and Python code, and why scans go wrong.

4 min read

A scanned EAN-13 is correct when its 13th digit equals the check digit computed from the first 12. Multiply the digits alternately by 1 and 3 starting from the left, add them up, and the check digit is (10 − sum mod 10) mod 10. If it does not match, the scan or the print is wrong: rescan, do not guess.

StepExample: 4006381333931
StepTake the first 12 digitsExample: 4006381333931400638133393
StepWeights, left to rightExample: 40063813339311 3 1 3 1 3 1 3 1 3 1 3
StepProductsExample: 40063813339314 0 0 18 3 24 1 9 3 9 9 9
StepSumExample: 400638133393189
StepCheck digit = (10 − 89 mod 10) mod 10Example: 40063813339311
StepPrinted 13th digitExample: 40063813339311 → valid

The check-digit rule in one example

The weights are 1 and 3 so that two common mistakes are caught: one wrong digit always changes the sum, and swapping two neighbours changes it too unless their difference is a multiple of 5 (for example 0 and 5). That is why a check digit catches most, not all, errors. It detects a bad read; it does not prove the product is the one you expect.

The same rule covers EAN-8 and UPC-A. Count the weights from the right instead: the digit next to the check digit gets weight 3, then 1, 3, 1... For EAN-13 that is the same result as counting from the left. A UPC-A code is an EAN-13 with a leading 0.

Code: validate EAN-13, EAN-8 and UPC-A

Tested against 4006381333931 (EAN-13), 96385074 (EAN-8) and 036000291452 (UPC-A): all valid. Changing the last digit of any of them makes it invalid.

python
def is_valid_gtin(code: str) -> bool:
    if not code.isdigit() or len(code) not in (8, 12, 13):
        return False
    digits = [int(c) for c in code]
    total = sum(d * (3 if i % 2 == 0 else 1) for i, d in enumerate(reversed(digits[:-1])))
    return (10 - total % 10) % 10 == digits[-1]
js
function isValidGtin(code) {
  if (!/^\d+$/.test(code) || ![8, 12, 13].includes(code.length)) return false
  const digits = [...code].map(Number)
  const check = digits.pop()
  const total = digits.reverse().reduce((sum, d, i) => sum + d * (i % 2 === 0 ? 3 : 1), 0)
  return (10 - (total % 10)) % 10 === check
}

Both return false for the right length with a wrong check digit, and for anything that is not 8, 12 or 13 digits. (GTIN-14 uses the same rule; add 14 to the lengths if you need it.)

Why scans fail

  • Blur or low resolution: bars merge, so a digit is read as its neighbour.
  • Glare or a curved surface: part of the symbol is lost.
  • Damaged or low-contrast print: thin bars disappear.
  • A missing quiet zone: no white margin around the symbol, so the scanner cannot find its edges.
  • Human typing: the digits under the bars retyped by hand. The check digit catches this too.

A failed check is the signal to rescan or retype. Do not "fix" the last digit by replacing it with the computed one: that makes the number valid but not necessarily the right product.

Decode from a photo instead of a scanner

Veralens is a document extraction API (OCR plus schema-typed JSON). If your input is a photo or a PDF page rather than a live scanner, declare the field with format: "barcode" and Veralens returns the decoded payload as printed. QR, Micro QR and the linear families EAN, UPC, Code 128, Code 39 and ITF are supported, and a page with no readable code returns null (Schemas and formats).

json
{
  "type": "object",
  "properties": {
    "product_ean": { "type": "string", "format": "barcode", "description": "EAN printed under the barcode" }
  }
}

Run the check digit on the returned value with the functions above. The API documentation does not list a check-digit validation for barcode fields (unlike iban, which is mod-97 verified), so do not rely on it; validate in your own code. Step-by-step request and job polling: Getting started. More on reading codes from documents: Extract QR codes and barcodes from documents. Using EANs in an app: barcode pantry tracker.

FAQ

  • Does a valid check digit mean the product exists? No. It only means the digits are internally consistent. Look the code up in a product database to confirm it.
  • Is a UPC-A the same as an EAN-13? A UPC-A is an EAN-13 with a leading 0, and the check digit is the same.

Try it on your own documents

Add funds from $5, create a key, and send your first document. Every job reports exactly what it cost.

Add $5, get an API key

Keep reading