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
| Character | Value |
|---|---|
0–9 | 0–9 |
A B C … Z | 10, 11, 12 … 35 |
< | 0 |
Worked example
The document number from the ICAO specimen passport is L898902C3.
| Character | Value | Weight | Product |
|---|---|---|---|
| L | 21 | 7 | 147 |
| 8 | 8 | 3 | 24 |
| 9 | 9 | 1 | 9 |
| 8 | 8 | 7 | 56 |
| 9 | 9 | 3 | 27 |
| 0 | 0 | 1 | 0 |
| 2 | 2 | 7 | 14 |
| C | 12 | 3 | 36 |
| 3 | 3 | 1 | 3 |
| Sum | 316 | ||
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.