Saltar al contenido
Constaia

MRZ ICAO 9303: cómo validar DNI y pasaportes

Formatos TD1, TD2 y TD3, dígitos de control con pesos 7-3-1 y módulo 10, ejemplo con el espécimen oficial de ICAO y código JavaScript para validar la MRZ.

Por Equipo Constaia8 min de lectura

También en: English, Português, Français

La zona de lectura mecánica (MRZ) son esas dos o tres líneas llenas de < al pie de un pasaporte o en el reverso de un DNI. Está pensada para que una máquina la lea sin errores, y por eso lleva dígitos de control: si el OCR confunde un carácter, la cuenta no cuadra.

Este artículo explica los formatos, el algoritmo de los dígitos de control con un ejemplo real del documento de ICAO, código para validarla y, sobre todo, lo que una MRZ válida no demuestra.

La norma: ICAO Doc 9303

La MRZ la define el Doc 9303 de ICAO (8.ª edición). La Parte 3 fija lo común a todos los documentos; las Partes 4 a 7 fijan cada formato. Lo esencial:

  • Solo se usan A–Z, 0–9 y el carácter de relleno <, impresos en tipografía OCR-B.
  • Las fechas van como AAMMDD: el 12 de julio de 1942 se escribe 420712.
  • Los nombres se transliteran sin tildes. Por ejemplo, la tabla de ICAO permite escribir la Ñ como N o como NXX.

Tres formatos

FormatoLíneas × caracteresUso típicoParte del Doc 9303
TD13 × 30Tarjetas de identidad (tamaño tarjeta de crédito), como el DNI españolParte 5
TD22 × 36Algunos documentos oficiales de tamaño intermedioParte 6
TD32 × 44PasaportesParte 4

TD2 se parece a TD3 con menos espacio: el campo de nombre es más corto y tiene un dígito de control menos. En la práctica, lo que más verás en formularios europeos es TD1 (DNI y tarjetas de identidad) y TD3 (pasaportes).

El dígito de control: pesos 7-3-1 y módulo 10

La Parte 3 (apartado 4.9) describe el cálculo:

  1. Cada carácter tiene un valor: las cifras valen lo que indican, < vale 0 y las letras van de A = 10 a Z = 35.
  2. Se multiplica cada valor por un peso que se repite: 7, 3, 1, 7, 3, 1…
  3. Se suman los productos.
  4. El dígito de control es el resto de dividir entre 10.
ABCDEFGHIJKLM
10111213141516171819202122
NOPQRSTUVWXYZ
23242526272829303132333435

El ejemplo del apéndice A de la Parte 3: la fecha 27 de julio de 1952 (520727).

Carácter520727
Peso731731
Producto35604967

Suma: 103. Resto de dividir entre 10: 3. En la MRZ se escribe 5207273.

Ejemplo trabajado: el pasaporte espécimen de ICAO (TD3)

El apéndice A de la Parte 4 muestra un pasaporte ficticio del estado "Utopía" (código UTO) a nombre de Anna Maria Eriksson:

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

La primera línea contiene el tipo de documento (PP), el estado emisor (UTO) y el nombre: apellido, <<, nombres separados por <. La segunda línea lleva los datos con sus dígitos de control:

PosicionesCampoValorControl
1–9Número de documentoL898902C310: 6
11–13NacionalidadUTO—
14–19Fecha de nacimiento74081220: 2
21SexoF—
22–27Fecha de caducidad34041528: 9
29–42Número personal / datos opcionalesZE184226B<<<<<43: 1
44Control compuesto—6

Comprobemos el número de documento, L898902C3:

CarácterL898902C3
Valor21898902123
Peso731731731
Producto1472495627014363

Suma: 316 → resto 6. Coincide con la posición 10.

Del mismo modo, la fecha de nacimiento 740812 suma 122 (control 2), la caducidad 340415 suma 69 (control 9) y el número personal suma 401 (control 1).

El dígito compuesto

El último carácter protege toda la línea. Se calcula sobre las posiciones 1–10, 14–20 y 22–43 de la segunda línea, es decir, los campos con sus propios dígitos de control, saltándose nacionalidad y sexo:

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

La suma ponderada es 896 → control compuesto 6, que es el último carácter de la línea.

Si el campo de número personal está vacío (todo <), su dígito de control puede ser < o 0. Tenlo en cuenta al validar.

TD1: el formato de las tarjetas de identidad

El espécimen de la Parte 5 es una tarjeta de la misma titular:

I<UTOD231458907<<<<<<<<<<<<<<<
7408122F1204159UTO<<<<<<<<<<<6
ERIKSSON<<ANNA<MARIA<<<<<<<<<<
  • Línea 1: tipo (I<), estado emisor (UTO), número de documento en las posiciones 6–14 (D23145890) y su control en la 15 (7); de la 16 a la 30, datos opcionales.
  • Línea 2: nacimiento (1–6) y control (7), sexo (8), caducidad (9–14) y control (15), nacionalidad (16–18), datos opcionales (19–29) y control compuesto (30).
  • Línea 3: el nombre.

El compuesto de TD1 se calcula sobre las posiciones 6–30 de la línea 1 y 1–7, 9–15 y 19–29 de la línea 2. En el espécimen da 6.

Un detalle: si el número de documento tiene más de 9 caracteres, en la posición 15 va un < y el número continúa en la zona de datos opcionales, con su dígito de control al final.

Código en JavaScript

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(`Carácter no válido en la MRZ: ${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: dos líneas de 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 }

Si cambias la caducidad a 340416, fallan el control de caducidad y el compuesto. Así detectas un error de lectura (o una edición torpe).

Lo que el código anterior no cubre

  • Errores de OCR: O/0, I/1, B/8 o S/5 se confunden a menudo. En campos que solo pueden ser numéricos (fechas, dígitos de control) puedes corregir con seguridad; en el número de documento, que es alfanumérico, puedes probar sustituciones y quedarte con la que cuadra.
  • El siglo: 74 puede ser 1974 o 2074. Para el nacimiento, elige el año que no quede en el futuro; para la caducidad, el que tenga sentido con la validez del documento.
  • Las líneas cortadas: si una foto recorta el final de la línea, rellena con < solo si sabes que faltaban rellenos, y marca el resultado como dudoso.

Coherencia entre la MRZ y la zona visual

Que la MRZ cuadre no basta: tiene que coincidir con lo que está impreso en la zona de inspección visual (VIZ). Compara al menos:

  • número de documento,
  • fecha de nacimiento y de caducidad,
  • sexo y nacionalidad,
  • apellidos y nombre, teniendo en cuenta la transliteración (sin tildes, < como espacio, Ñ como N o NXX) y que los nombres largos pueden ir truncados.

Una MRZ correcta con datos distintos a los impresos es una señal clara para revisar el documento a mano.

Advertencia: un checksum no prueba autenticidad

El algoritmo es público y sencillo. Existen generadores públicos que producen MRZ con todos los dígitos de control correctos. Por tanto:

  • Una MRZ válida solo demuestra que los caracteres son coherentes entre sí.
  • No demuestra que el documento exista, que no esté anulado ni que pertenezca a quien lo presenta.
  • La autenticidad se comprueba por otras vías (por ejemplo, los elementos de seguridad físicos o el chip del documento), que quedan fuera de lo que muestra una foto.

Cómo lo hace Constaia

Cuando analizas un DNI, un NIE o un pasaporte, Constaia lee la MRZ con OCR, corrige las confusiones típicas y devuelve dos comprobaciones deterministas: mrz_checksums (todos los dígitos de control) y mrz_matches_visual (la MRZ coincide con los datos impresos). Si algo no cuadra, el veredicto pasa a invalid o review con el motivo.

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"}'

Con una clave ck_test_… y el fichero passport.jpg obtienes una respuesta simulada sin consumir créditos. El validador de MRZ es parte del paquete open source @constaia/validators. Y lo repetimos: Constaia no es KYC biométrico y un checksum correcto no demuestra que un documento sea auténtico. Más información en Checks.

Resumen

  • TD1 = 3 × 30 (tarjetas de identidad), TD2 = 2 × 36, TD3 = 2 × 44 (pasaportes).
  • Dígito de control: valores (A = 10 … Z = 35, < = 0), pesos 7-3-1, suma, módulo 10.
  • Valida cada campo y el compuesto, y compara la MRZ con la zona visual.
  • Un checksum correcto no prueba autenticidad.

¿Quieres probar con una MRZ concreta? Usa el lector de MRZ gratuito. Para integrarlo en tu producto, crea una cuenta gratis con 250 créditos al mes.

Fuentes

  1. 01ICAO Doc 9303, 8.ª ed., Parte 3: especificaciones comunes a todos los MRTD
  2. 02ICAO Doc 9303, 8.ª ed., Parte 4: pasaportes (TD3)
  3. 03ICAO Doc 9303, 8.ª ed., Parte 5: documentos de tamaño TD1
  4. 04Wikipedia — Machine-readable passport
  5. 05Ejemplo público de generador de MRZ (GitHub)