How the MRZ check digit works

Every important field in a machine readable zone is followed by one check digit. It is calculated with a repeating 7-3-1 weighting, defined in ICAO Doc 9303 Part 3.

Check digit: 6

The algorithm in three steps

First, turn every character into a number. Digits keep their value, letters A to Z become 10 to 35, and the filler < counts as 0.

Second, multiply each number by a weight. The weights repeat 7, 3, 1, 7, 3, 1 from the first character of the field to the last.

Third, add up all the products and take the remainder after dividing by 10. That remainder is the check digit.

Character values

CharacterValue
0–90–9
A B C … Z10, 11, 12 … 35
<0

Worked example

The document number from the ICAO specimen passport is L898902C3.

CharacterValueWeightProduct
L217147
88324
9919
88756
99327
0010
22714
C12336
3313
Sum316

The sum is 316. Divided by 10 the remainder is 6, so the check digit is 6. In the specimen MRZ the number is printed as L898902C36.

Which fields have check digits

The document number, the date of birth and the date of expiry always have their own check digit. TD3 passports also check the optional data (personal number). A final composite check digit covers the document number, date of birth, date of expiry and optional data together, with their own check digits included. Visas (MRV-A and MRV-B) have no composite check digit.

When the optional data on a TD3 passport is entirely filler, its check digit may be either < or 0. Both are valid.

Code

JavaScript

function mrzCheckDigit(s) {
  const w = [7, 3, 1];
  let sum = 0;
  for (let i = 0; i < s.length; i++) {
    const c = s[i];
    const v = c === '<' ? 0 : /\d/.test(c) ? +c : c.charCodeAt(0) - 55;
    sum += v * w[i % 3];
  }
  return sum % 10;
}

mrzCheckDigit('L898902C3'); // 6

Python

def mrz_check_digit(s: str) -> int:
    weights = (7, 3, 1)
    total = 0
    for i, c in enumerate(s):
        v = 0 if c == '<' else int(c) if c.isdigit() else ord(c) - 55
        total += v * weights[i % 3]
    return total % 10

mrz_check_digit('L898902C3')  # 6

Common mistakes

Counting letters from 1 instead of 10, starting the weights at the wrong character, or leaving the field’s own check digits out of the composite calculation. To test a full MRZ rather than one field, paste it into the MRZ checker.