ICAO 9303 · TD3 passports
TD3 is the ICAO 9303 travel document format used by most ePassports: two lines of 44 characters designed for fast, interoperable optical reading. This guide explains why TD3 exists, how it is structured, what each position means, and how check digits enforce integrity.
Type of document. TD3 passports use P (e.g., P<).
Non-passport codes exist (e.g., V for visas) but TD3 expects P.
Three-letter ICAO code of issuing state or authority.
Primary surname(s) followed by << then given/secondary names separated by <.
Only A–Z and < allowed; diacritics stripped; pad with < to fill.
Passport number, alphanumeric; padded with < if short.
Col 10 is its check digit.
Three-letter ICAO country code of holder nationality.
YYMMDD with implied century; col 20 is DOB check digit.
M, F, or < (unspecified).
YYMMDD expiry; col 28 is expiry check digit.
Issuer-defined personal number / optional data; col 43 is its check digit.
Often used for national ID or internal control numbers.
Check digit over concatenation: document number + digit, DOB + digit, expiry + digit, optional data + digit.
Validates cross-field integrity.
Input: L00000000
Values: L=21, 0=0 ...
Weights: 7,3,1,7,3,1,7,3,1
Sum: (21×7) + (0×3) + ... = 147
147 mod 10 = 7 → check digit
Concatenate: document number + its digit, DOB + digit, expiry + digit, optional data + digit. Apply the same weighting and modulus. This binds the MRZ into one integrity value.
P<UTODOE<<JANE<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<L000000007UTO9001011F3001019AA0000000<<<<<02All check digits valid; uses filler < to pad names and optional data.
P<GBRTESTER<<ALICIA<<<<<<<<<<<<<<<<<<<<<<<<<C987654320GBR7505205F2905202CC0000000<<<<<05Composite digit intentionally wrong (should be 4). Prior field digits still valid.
mrz-fast
A high-performance, zero-dependency TypeScript library for parsing and generating passport MRZ codes (TD3 format). Supports ICAO 9303 TD3—the standard MRZ used in international passports.
npm install mrz-fastimport { parseMRZ } from 'mrz-fast';
const mrz: [string, string] = [
'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<',
'L898902C36UTO7408122F1204159ZE184226B<<<<<10'
];
const result = parseMRZ(mrz);
console.log(result.valid); // true
console.log(result.fields.firstName); // "ANNA MARIA"import { createMRZ } from 'mrz-fast';
const data = {
documentCode: 'P',
issuingState: 'UTO',
lastName: 'ERIKSSON',
firstName: 'ANNA MARIA',
documentNumber: 'L898902C3',
nationality: 'UTO',
birthDate: { year: 1974, month: 8, day: 12 },
sex: 'female',
expirationDate: { year: 2012, month: 4, day: 15 },
personalNumber: 'ZE184226B', // optional
};
const [line1, line2] = createMRZ(data);import { parseMRZ } from 'mrz-fast';
const messy: [string, string] = [
'P<UTOERIKSSON<<ANNA<MARIA', // too short, padded
'L8989O2C36UTO74O8I22FI2O4I59ZEI84226B<<<<<IO'
];
const result = parseMRZ(messy, { errorCorrection: true });
console.log(result.valid, result.corrected); // true true
console.log(result.correctionMetrics);parseMRZ(mrz, options?) → fields, lines (corrected if enabled), details, optional correction metrics.createMRZ(data) → [line1, line2] with check digits.errorCorrection?: boolean (default false).ParseResult with per-field errors and check digits.