Skip to content
Constaia

ICAO 9303 MRZ: how to validate ID cards and passports

TD1, TD2 and TD3 formats, 7-3-1 weighted modulo 10 check digits, a worked example with the official ICAO specimen and JavaScript code to validate an MRZ.

By Constaia team8 min read

Also in: Español, Português, Français

The machine readable zone (MRZ) is the two or three lines full of < at the bottom of a passport page or on the back of an ID card. It is designed to be read by machines without errors, which is why it carries check digits: if OCR misreads a character, the maths no longer adds up.

This article covers the formats, the check digit algorithm with a real example from the ICAO document, code to validate an MRZ and, above all, what a valid MRZ does not prove.

The standard: ICAO Doc 9303

The MRZ is defined by ICAO Doc 9303 (8th edition). Part 3 covers what is common to all documents; Parts 4 to 7 cover each format. The essentials:

  • Only A–Z, 0–9 and the filler character < are used, printed in the OCR-B typeface.
  • Dates are written as YYMMDD: 12 July 1942 becomes 420712.
  • Names are transliterated without diacritics. For example, the ICAO table allows the Spanish Ñ to be written as N or NXX.

Three formats

FormatLines × charactersTypical useDoc 9303 part
TD13 × 30ID cards (credit card size), such as the Spanish DNIPart 5
TD22 × 36Some mid-size official documentsPart 6
TD32 × 44PassportsPart 4

TD2 looks like TD3 with less room: a shorter name field and one check digit fewer. In practice, what you will see most in European forms is TD1 (national ID cards) and TD3 (passports).

The check digit: 7-3-1 weights, modulo 10

Part 3 (section 4.9) describes the calculation:

  1. Each character has a value: digits keep their value, < is 0 and letters go from A = 10 to Z = 35.
  2. Multiply each value by a repeating weight: 7, 3, 1, 7, 3, 1…
  3. Add up the products.
  4. The check digit is the remainder after dividing by 10.
ABCDEFGHIJKLM
10111213141516171819202122
NOPQRSTUVWXYZ
23242526272829303132333435

The example in Appendix A to Part 3: the date 27 July 1952 (520727).

Character520727
Weight731731
Product35604967

Sum: 103. Remainder after dividing by 10: 3. In the MRZ it is written 5207273.

Worked example: the ICAO specimen passport (TD3)

Appendix A to Part 4 shows a fictitious passport from the state of "Utopia" (code UTO) issued to Anna Maria Eriksson:

PPUTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<
L898902C36UTO7408122F3404159ZE184226B<<<<<16

The first line holds the document type (PP), the issuing state (UTO) and the name: surname, <<, then given names separated by <. The second line carries the data with their check digits:

PositionsFieldValueCheck
1–9Document numberL898902C310: 6
11–13NationalityUTO—
14–19Date of birth74081220: 2
21SexF—
22–27Date of expiry34041528: 9
29–42Personal number / optional dataZE184226B<<<<<43: 1
44Composite check—6

Let us check the document number, L898902C3:

CharacterL898902C3
Value21898902123
Weight731731731
Product1472495627014363

Sum: 316 → remainder 6. It matches position 10.

In the same way, the date of birth 740812 sums to 122 (check 2), the expiry 340415 sums to 69 (check 9) and the personal number sums to 401 (check 1).

The composite check digit

The last character protects the whole line. It is computed over positions 1–10, 14–20 and 22–43 of the second line, i.e. the fields with their own check digits, skipping nationality and sex:

L898902C36 + 7408122 + 3404159ZE184226B<<<<<1

The weighted sum is 896 → composite check 6, the last character of the line.

If the personal number field is empty (all <), its check digit may be < or 0. Allow for both when validating.

TD1: the ID card format

The specimen in Part 5 is a card for the same holder:

I<UTOD231458907<<<<<<<<<<<<<<<
7408122F1204159UTO<<<<<<<<<<<6
ERIKSSON<<ANNA<MARIA<<<<<<<<<<
  • Line 1: type (I<), issuing state (UTO), document number in positions 6–14 (D23145890) and its check digit in 15 (7); positions 16–30 hold optional data.
  • Line 2: date of birth (1–6) and check (7), sex (8), expiry (9–14) and check (15), nationality (16–18), optional data (19–29) and composite check (30).
  • Line 3: the name.

The TD1 composite is computed over positions 6–30 of line 1 and 1–7, 9–15 and 19–29 of line 2. In the specimen it is 6.

One detail: if the document number is longer than 9 characters, position 15 holds a < and the number continues in the optional data area, with its check digit at the end.

JavaScript code

mrz.ts
const WEIGHTS = [7, 3, 1];

function charValue(c: string): number {
  if (c === "<") return 0;
  if (c >= "0" && c <= "9") return c.charCodeAt(0) - 48; // 0–9
  if (c >= "A" && c <= "Z") return c.charCodeAt(0) - 55; // A=10 … Z=35
  throw new Error(`Invalid MRZ character: ${c}`);
}

export function checkDigit(data: string): string {
  let sum = 0;
  for (let i = 0; i < data.length; i++) sum += charValue(data[i]) * WEIGHTS[i % 3];
  return String(sum % 10);
}

export function validateTd3(line1: string, line2: string) {
  if (line1.length !== 44 || line2.length !== 44) throw new Error("TD3: two lines of 44");
  const fields: Record<string, [string, string]> = {
    document_number: [line2.slice(0, 9), line2[9]],
    birth_date: [line2.slice(13, 19), line2[19]],
    expiry_date: [line2.slice(21, 27), line2[27]],
    personal_number: [line2.slice(28, 42), line2[42]],
    composite: [line2.slice(0, 10) + line2.slice(13, 20) + line2.slice(21, 43), line2[43]],
  };
  return Object.fromEntries(
    Object.entries(fields).map(([name, [data, digit]]) => [name, checkDigit(data) === digit]),
  );
}

validateTd3(
  "PPUTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<",
  "L898902C36UTO7408122F3404159ZE184226B<<<<<16",
);
// { document_number: true, birth_date: true, expiry_date: true,
//   personal_number: true, composite: true }

If you change the expiry to 340416, both the expiry check and the composite fail. That is how you catch a misread (or a clumsy edit).

What the code above does not cover

  • OCR errors: O/0, I/1, B/8 and S/5 are often confused. In fields that can only be numeric (dates, check digits) you can correct safely; in the document number, which is alphanumeric, you can try substitutions and keep the one that matches.
  • The century: 74 could be 1974 or 2074. For the date of birth, choose the year that is not in the future; for expiry, the one consistent with the document's validity.
  • Cropped lines: if a photo cuts off the end of a line, pad with < only if you know fillers were missing, and flag the result as uncertain.

Consistency between the MRZ and the visual zone

A consistent MRZ is not enough: it has to match what is printed in the visual inspection zone (VIZ). Compare at least:

  • document number,
  • dates of birth and expiry,
  • sex and nationality,
  • surname and given names, allowing for transliteration (no diacritics, < as a space, Ñ as N or NXX) and for long names being truncated.

A correct MRZ with data that differs from the printed data is a clear signal to review the document by hand.

Warning: a checksum does not prove authenticity

The algorithm is public and simple. There are public generators that produce MRZs with every check digit correct. Therefore:

  • A valid MRZ only shows that the characters are consistent with each other.
  • It does not show that the document exists, that it has not been cancelled or that it belongs to the person presenting it.
  • Authenticity is checked by other means (for example physical security features or the document's chip), which are beyond what a photo shows.

How Constaia does it

When you analyse a DNI, NIE or passport, Constaia reads the MRZ with OCR, fixes typical misreads and returns two deterministic checks: mrz_checksums (all check digits) and mrz_matches_visual (the MRZ matches the printed data). If something does not add up, the verdict becomes invalid or review with the reason.

curl https://api.constaia.com/v1/analyze \
  -H "Authorization: Bearer ck_test_..." \
  -F file=@passport.jpg \
  -F 'options={"expect":"passport","checks":{"not_expired":true},"storage":"none"}'

With a ck_test_… key and the file passport.jpg you get a simulated response without spending credits. The MRZ validator is part of the open source package @constaia/validators. And once more: Constaia is not biometric KYC, and a correct checksum does not prove a document is authentic. More in Checks.

Summary

  • TD1 = 3 × 30 (ID cards), TD2 = 2 × 36, TD3 = 2 × 44 (passports).
  • Check digit: values (A = 10 … Z = 35, < = 0), weights 7-3-1, sum, modulo 10.
  • Validate each field and the composite, and compare the MRZ with the visual zone.
  • A correct checksum does not prove authenticity.

Want to try a specific MRZ? Use the free MRZ reader. To build it into your product, create a free account with 250 credits a month.

Sources

  1. 01ICAO Doc 9303, 8th ed., Part 3: Specifications common to all MRTDs
  2. 02ICAO Doc 9303, 8th ed., Part 4: Machine readable passports (TD3)
  3. 03ICAO Doc 9303, 8th ed., Part 5: TD1 size documents
  4. 04Wikipedia — Machine-readable passport
  5. 05Public example of an MRZ generator (GitHub)