Skip to main content

Passport MRZ Test Data Generator and Check Digit Validator

MOCK DATA

ICAO 9303 MRZ test fixtures — TD1/TD2/TD3/MRV lines with correct 7-3-1 check digits and exact geometry, issued by the specimen state UTO, plus a validator.

Generated test data for fixtures and development

Base URL
/api/mrz
Category
People
Capabilities
2 routes Seedable 8 locales
Last updated
July 29, 2026

GET /api/mrz

Each record is one complete machine readable zone plus the fields it encodes. Issuing State and nationality are always UTO and the document number is always SPECIMEN plus a digit.

Live requestRuns against the public API
No key required
GET/api/mrz

Request parameters

MRZ document format. td1 = 3 lines of 30, td2 = 2 lines of 36, td3 = 2 lines of 44 (the passport layout), mrva = 2 lines of 44 and mrvb = 2 lines of 36 (machine readable visas).

Sex written to the MRZ. M and F are written verbatim; X writes the filler '<', which is what Doc 9303 requires in the MRZ when the printed page shows X. Omit for a mix.

true forces an expiry date strictly before the asOf anchor, false forces one strictly after. Omit for a realistic mix of roughly one expired document in sixteen.

true builds a name long enough to overflow the field, forcing the Doc 9303-4 Section 4.2.3 truncation path (the field is filled to the last position and ends with a letter).

Youngest holder age in completed years at the asOf anchor. Must be ≤ maxAge.

Oldest holder age in completed years at the asOf anchor. Must be ≥ minAge.

Replayable evaluation anchor (UTC midnight) for ageYears and for expired. Defaults to today; must be between 1970-01-01 and 2099-12-31. The resolved anchor is echoed in meta.params.

Advanced response options8 options

How many records to generate (1–100).

Deterministic output: the same seed always returns the same records. Omit for random (the used seed is echoed in meta.seed).

Locale for the generated data (names, addresses, …).

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Response format: json envelope, ndjson (one record per line) or csv.

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/mrz"
const res = await fetch("https://randomapi.dev/api/mrz");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/mrz").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/mrz"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 7
type enum

MRZ document format. td1 = 3 lines of 30, td2 = 2 lines of 36, td3 = 2 lines of 44 (the passport layout), mrva = 2 lines of 44 and mrvb = 2 lines of 36 (machine readable visas).

default: td3
allowed: td1 | td2 | td3 | mrva | mrvb
example: type=td1
sex enum

Sex written to the MRZ. M and F are written verbatim; X writes the filler '<', which is what Doc 9303 requires in the MRZ when the printed page shows X. Omit for a mix.

allowed: M | F | X
example: sex=F
expired boolean

true forces an expiry date strictly before the asOf anchor, false forces one strictly after. Omit for a realistic mix of roughly one expired document in sixteen.

example: expired=true
longName boolean

true builds a name long enough to overflow the field, forcing the Doc 9303-4 Section 4.2.3 truncation path (the field is filled to the last position and ends with a letter).

default: false
example: longName=true
minAge int

Youngest holder age in completed years at the asOf anchor. Must be ≤ maxAge.

default: 18
allowed: 0 – 120
example: minAge=25
maxAge int

Oldest holder age in completed years at the asOf anchor. Must be ≥ minAge.

default: 80
allowed: 0 – 120
example: maxAge=40
asOf date

Replayable evaluation anchor (UTC midnight) for ageYears and for expired. Defaults to today; must be between 1970-01-01 and 2099-12-31. The resolved anchor is echoed in meta.params.

example: asOf=2026-07-01
Universal parameters Shared response and formatting options 8
count int

How many records to generate (1–100).

default: 10
allowed: 1 – 100
example: count=3
seed int

Deterministic output: the same seed always returns the same records. Omit for random (the used seed is echoed in meta.seed).

example: seed=42
locale enum

Locale for the generated data (names, addresses, …).

default: en
allowed: en | da | de | fr | es | it | pt | ja
example: locale=da
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=type,mrz
exclude list

Return all fields except these (comma-separated).

example: exclude=checkDigits
format enum

Response format: json envelope, ndjson (one record per line) or csv.

default: json
allowed: json | ndjson | csv
example: format=csv
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 19
type string

MRZ document format of this record: td1, td2, td3, mrva or mrvb.

example: td3

mrz string

The complete machine readable zone, lines joined with a newline.

example: PPUTOKLOCKO<<ADALINE<<<<<<<<<<<<<<<<<<<<<<<< SPECIMEN91UTO9607011F2907022<<<<<<<<<<<<<<02

lines string[]

The individual machine readable lines, each exactly lineLength characters.

example: ["PPUTOKLOCKO<<ADALINE<<<<<<<<<<<<<<<<<<<<<<<<","SPECIMEN91UTO9607011F2907022<<<<<<<<<<<<<<02"]

lineLength integer

Exact character count of every line in this format (30, 36 or 44).

example: 44

documentCode string

Positions 1-2 of the first line: PP for the TD3 passport, I< for TD1/TD2, V< for a visa.

example: PP

issuingState string

Issuing State code — always UTO, the ICAO specimen code for Utopia.

example: UTO

nationality string

Nationality code — always UTO, the ICAO specimen code for Utopia.

example: UTO

documentNumber string

Nine-character synthetic document number: the literal word SPECIMEN plus one digit.

example: SPECIMEN9

primaryIdentifier string

Full transliterated primary identifier (surname), before any truncation.

example: KLOCKO

secondaryIdentifier string

Full transliterated secondary identifier (given names), before any truncation. Empty for the roughly one record in eight that has no secondary identifier at all.

example: ADALINE

nameField string

The name field exactly as written in the MRZ, padded or truncated to its fixed width.

example: KLOCKO<<ADALINE<<<<<<<<<<<<<<<<<<<<<<<<

nameTruncated boolean

Whether the assembled name did not fit the field and was shortened.

example: false

sex string

MRZ sex character: M, F or < for unspecified (the printed page shows X).

example: F

dateOfBirth string (ISO 8601 date)

Date of birth in full; the MRZ itself carries only the six digits YYMMDD.

example: 1996-07-01

dateOfExpiry string (ISO 8601 date)

Expiry date in full; the MRZ itself carries only the six digits YYMMDD.

example: 2029-07-02

ageYears integer

Completed years between dateOfBirth and the resolved asOf anchor.

example: 30

expired boolean

Whether dateOfExpiry is earlier than the resolved asOf anchor.

example: false

optionalData string[]

Optional-data / personal-number field(s) in MRZ order — always all filler here (TD1 has two, every other format one).

example: ["<<<<<<<<<<<<<<"]

checkDigits object

Every 7-3-1 check digit in the record: documentNumber, dateOfBirth, dateOfExpiry, optionalData (TD3 only) and composite (null for visas).

example: {"documentNumber":"1","dateOfBirth":"1","dateOfExpiry":"2","optionalData":"0","composite":"2"}

Documented examples

Build-generated requests and complete responses
6
Ten passport (TD3) MRZs
GET /api/mrz
{
  "data": [
    {
      "type": "td3",
      "mrz": "PPUTOMACGYVER<<DORIAN<<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN02UTO7804034M2910079<<<<<<<<<<<<<<08",
      "lines": [
        "PPUTOMACGYVER<<DORIAN<<<<<<<<<<<<<<<<<<<<<<<",
        "SPECIMEN02UTO7804034M2910079<<<<<<<<<<<<<<08"
      ],
      "lineLength": 44,
      "documentCode": "PP",
      "issuingState": "UTO",
      "nationality": "UTO",
      "documentNumber": "SPECIMEN0",
      "primaryIdentifier": "MACGYVER",
      "secondaryIdentifier": "DORIAN",
      "nameField": "MACGYVER<<DORIAN<<<<<<<<<<<<<<<<<<<<<<<",
      "nameTruncated": false,
      "sex": "M",
      "dateOfBirth": "1978-04-03",
      "dateOfExpiry": "2029-10-07",
      "ageYears": 48,
      "expired": false,
      "optionalData": [
        "<<<<<<<<<<<<<<"
      ],
      "checkDigits": {
        "documentNumber": "2",
        "dateOfBirth": "4",
        "dateOfExpiry": "9",
        "optionalData": "0",
        "composite": "8"
      }
    },
    {
      "type": "td3",
      "mrz": "PPUTOWISOZK<<LINDSEY<<<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN13UTO0508012M3111068<<<<<<<<<<<<<<06",
      "lines": [
        "PPUTOWISOZK<<LINDSEY<<<<<<<<<<<<<<<<<<<<<<<<",
        "SPECIMEN13UTO0508012M3111068<<<<<<<<<<<<<<06""generatedAt": "2026-07-29T08:13:22.000Z"
  }
}
Reproducible ID cards (TD1)
GET /api/mrz?type=td1&count=5&seed=42
{
  "data": [
    {
      "type": "td1",
      "mrz": "I<UTOSPECIMEN02<<<<<<<<<<<<<<<\n7804034M2910079UTO<<<<<<<<<<<8\nMACGYVER<<DORIAN<<<<<<<<<<<<<<",
      "lines": [
        "I<UTOSPECIMEN02<<<<<<<<<<<<<<<",
        "7804034M2910079UTO<<<<<<<<<<<8",
        "MACGYVER<<DORIAN<<<<<<<<<<<<<<"
      ],
      "lineLength": 30,
      "documentCode": "I<",
      "issuingState": "UTO",
      "nationality": "UTO",
      "documentNumber": "SPECIMEN0",
      "primaryIdentifier": "MACGYVER",
      "secondaryIdentifier": "DORIAN",
      "nameField": "MACGYVER<<DORIAN<<<<<<<<<<<<<<",
      "nameTruncated": false,
      "sex": "M",
      "dateOfBirth": "1978-04-03",
      "dateOfExpiry": "2029-10-07",
      "ageYears": 48,
      "expired": false,
      "optionalData": [
        "<<<<<<<<<<<<<<<",
        "<<<<<<<<<<<"
      ],
      "checkDigits": {
        "documentNumber": "2",
        "dateOfBirth": "4",
        "dateOfExpiry": "9",
        "optionalData": null,
        "composite": "8"
      }
    },
    {
      "type": "td1",
      "mrz": "I<UTOSPECIMEN13<<<<<<<<<<<<<<<\n0508012M3111068UTO<<<<<<<<<<<6\nWISOZK<<LINDSEY<<<<<<<<<<<<<<<",
      "lines": [
    …
    "generatedAt": "2026-07-29T08:13:22.000Z"
  }
}
Expired visas for an adult range
GET /api/mrz?type=mrva&expired=true&minAge=25&maxAge=60&count=5
{
  "data": [
    {
      "type": "mrva",
      "mrz": "V<UTOMACGYVER<<DORIAN<<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN57UTO8404028M2207227<<<<<<<<<<<<<<<<",
      "lines": [
        "V<UTOMACGYVER<<DORIAN<<<<<<<<<<<<<<<<<<<<<<<",
        "SPECIMEN57UTO8404028M2207227<<<<<<<<<<<<<<<<"
      ],
      "lineLength": 44,
      "documentCode": "V<",
      "issuingState": "UTO",
      "nationality": "UTO",
      "documentNumber": "SPECIMEN5",
      "primaryIdentifier": "MACGYVER",
      "secondaryIdentifier": "DORIAN",
      "nameField": "MACGYVER<<DORIAN<<<<<<<<<<<<<<<<<<<<<<<",
      "nameTruncated": false,
      "sex": "M",
      "dateOfBirth": "1984-04-02",
      "dateOfExpiry": "2022-07-22",
      "ageYears": 42,
      "expired": true,
      "optionalData": [
        "<<<<<<<<<<<<<<<<"
      ],
      "checkDigits": {
        "documentNumber": "7",
        "dateOfBirth": "8",
        "dateOfExpiry": "7",
        "optionalData": null,
        "composite": null
      }
    },
    {
      "type": "mrva",
      "mrz": "V<UTOWISOZK<<LINDSEY<<<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN91UTO9908017M2101163<<<<<<<<<<<<<<<<",
      "lines": [
        "V<UTOWISOZK<<LINDSEY<<<<<<<<<<<<<<<<<<<<<<<<",
        "SPECIMEN91UTO9908017M2101163<<<<<<<<<<<<<<<<""generatedAt": "2026-07-29T08:13:22.000Z"
  }
}
Forced name truncation on TD2
GET /api/mrz?type=td2&longName=true&count=5&seed=7
{
  "data": [
    {
      "type": "td2",
      "mrz": "I<UTOBROWN<FAHEY<PRICE<<SKYLA<KRISTO\nSPECIMEN46UTO6606068F2805081<<<<<<<4",
      "lines": [
        "I<UTOBROWN<FAHEY<PRICE<<SKYLA<KRISTO",
        "SPECIMEN46UTO6606068F2805081<<<<<<<4"
      ],
      "lineLength": 36,
      "documentCode": "I<",
      "issuingState": "UTO",
      "nationality": "UTO",
      "documentNumber": "SPECIMEN4",
      "primaryIdentifier": "BROWN FAHEY PRICE",
      "secondaryIdentifier": "SKYLA KRISTOFER",
      "nameField": "BROWN<FAHEY<PRICE<<SKYLA<KRISTO",
      "nameTruncated": true,
      "sex": "F",
      "dateOfBirth": "1966-06-06",
      "dateOfExpiry": "2028-05-08",
      "ageYears": 60,
      "expired": false,
      "optionalData": [
        "<<<<<<<"
      ],
      "checkDigits": {
        "documentNumber": "6",
        "dateOfBirth": "8",
        "dateOfExpiry": "1",
        "optionalData": null,
        "composite": "4"
      }
    },
    {
      "type": "td2",
      "mrz": "I<UTOKUNZE<SCHUMM<SCHOEN<FARRELL<SCH\nSPECIMEN91UTO7203040F3406072<<<<<<<8",
      "lines": [
        "I<UTOKUNZE<SCHUMM<SCHOEN<FARRELL<SCH",
        "SPECIMEN91UTO7203040F3406072<<<<<<<8""generatedAt": "2026-07-29T08:13:22.000Z"
  }
}
German names transliterated into the MRZ
GET /api/mrz?locale=de&count=5&seed=2
{
  "data": [
    {
      "type": "td3",
      "mrz": "PPUTOSCHNUERER<<ANNEMARIE<<<<<<<<<<<<<<<<<<<\nSPECIMEN02UTO6005225F3210189<<<<<<<<<<<<<<00",
      "lines": [
        "PPUTOSCHNUERER<<ANNEMARIE<<<<<<<<<<<<<<<<<<<",
        "SPECIMEN02UTO6005225F3210189<<<<<<<<<<<<<<00"
      ],
      "lineLength": 44,
      "documentCode": "PP",
      "issuingState": "UTO",
      "nationality": "UTO",
      "documentNumber": "SPECIMEN0",
      "primaryIdentifier": "SCHNUERER",
      "secondaryIdentifier": "ANNEMARIE",
      "nameField": "SCHNUERER<<ANNEMARIE<<<<<<<<<<<<<<<<<<<",
      "nameTruncated": false,
      "sex": "F",
      "dateOfBirth": "1960-05-22",
      "dateOfExpiry": "2032-10-18",
      "ageYears": 66,
      "expired": false,
      "optionalData": [
        "<<<<<<<<<<<<<<"
      ],
      "checkDigits": {
        "documentNumber": "2",
        "dateOfBirth": "5",
        "dateOfExpiry": "9",
        "optionalData": "0",
        "composite": "0"
      }
    },
    {
      "type": "td3",
      "mrz": "PPUTOGOTTHARDT<<JESPER<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN35UTO9504305M3305223<<<<<<<<<<<<<<04",
      "lines": [
        "PPUTOGOTTHARDT<<JESPER<<<<<<<<<<<<<<<<<<<<<<",
        "SPECIMEN35UTO9504305M3305223<<<<<<<<<<<<<<04""generatedAt": "2026-07-29T08:13:22.000Z"
  }
}
Just the MRZ text, newline-delimited
GET /api/mrz?fields=mrz&format=ndjson&count=20
{"mrz":"PPUTOMACGYVER<<DORIAN<<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN02UTO7804034M2910079<<<<<<<<<<<<<<08"}
{"mrz":"PPUTOWISOZK<<LINDSEY<<<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN13UTO0508012M3111068<<<<<<<<<<<<<<06"}
{"mrz":"PPUTOJAKUBOWSKI<<SAIGE<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN02UTO5409114F3103304<<<<<<<<<<<<<<00"}
{"mrz":"PPUTORENNER<<HEIDI<<<<<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN80UTO9902288F1805040<<<<<<<<<<<<<<08"}
{"mrz":"PPUTORAU<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN68UTO5604160F2902049<<<<<<<<<<<<<<08"}
{"mrz":"PPUTOLYNCH<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN46UTO0612276F2905143<<<<<<<<<<<<<<08"}
{"mrz":"PPUTOMILLER<<SANFORD<<<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN35UTO0212182F3402063<<<<<<<<<<<<<<02"}
{"mrz":"PPUTOLANGWORTH<<AMPARO<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN46UTO5602100F3508172<<<<<<<<<<<<<<02"}
{"mrz":"PPUTOGOODWIN<<DENA<<<<<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN46UTO7908046M2708159<<<<<<<<<<<<<<08"}
{"mrz":"PPUTOSTIEDEMANN<<SAMANTHA<<<<<<<<<<<<<<<<<<<\nSPECIMEN46UTO4807159M3111219<<<<<<<<<<<<<<02"}
{"mrz":"PPUTOABERNATHY<<MYRL<<<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN79UTO4508065F3410185<<<<<<<<<<<<<<00"}
{"mrz":"PPUTOJACOBSON<<ROBERT<<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN80UTO9706073F3210167<<<<<<<<<<<<<<02"}
{"mrz":"PPUTOFAY<<BILLIE<<<<<<<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN35UTO0405100F3410174<<<<<<<<<<<<<<06"}
{"mrz":"PPUTOBASHIRIAN<<STEPHEN<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN02UTO7401226M3301029<<<<<<<<<<<<<<08"}
{"mrz":"PPUTOLOCKMAN<<MARCELLE<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN68UTO6107118M3208069<<<<<<<<<<<<<<08"}
{"mrz":"PPUTOSWANIAWSKI<<KAYLI<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN79UTO5611274F2705206<<<<<<<<<<<<<<02"}
{"mrz":"PPUTOPROHASKA<<ANISSA<<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN35UTO9301135M3309069<<<<<<<<<<<<<<00"}
{"mrz":"PPUTOMACEJKOVIC<<DINA<<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN80UTO7005071M3507175<<<<<<<<<<<<<<06"}
{"mrz":"PPUTOSENGER<<SEAN<<<<<<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN35UTO7808308F3106143<<<<<<<<<<<<<<08"}
{"mrz":"PPUTOULLRICH<<CONNER<<<<<<<<<<<<<<<<<<<<<<<<\nSPECIMEN35UTO7802100M3501047<<<<<<<<<<<<<<04"}

GET /api/mrz/validate

Structural check only: geometry, MRZ alphabet, sex, date fields and every check digit. It never throws on malformed input and never claims a document is authentic.

Live requestRuns against the public API
No key required
GET/api/mrz/validate?mrz=PPUTOERIKSSON%3C%3CANNA%3CMARIA%3C%3C%3C%3C%3C%3C%3C%3C%3C%3C%3C%3C%3C%3C%3C%3C%3C%3C%3C%0AL898902C36UTO7408122F1204159ZE184226B%3C%3C%3C%3C%3C10

Request parameters

The MRZ lines, separated by newlines (%0A) or by '|'. One unbroken 90, 88 or 72-character blob is split by the only geometry it can be. ASCII lower-case letters are upper-cased; anything else stays visible and invalid.

MRZ document format. td1 = 3 lines of 30, td2 = 2 lines of 36, td3 = 2 lines of 44 (the passport layout), mrva = 2 lines of 44 and mrvb = 2 lines of 36 (machine readable visas). Omit to detect the format from the line count, the line length and the leading V of a visa.

Advanced response options5 options

Locale for the generated data (names, addresses, …).

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/mrz/validate?mrz=PPUTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<%0AL898902C36UTO7408122F1204159ZE184226B<<<<<10"
const res = await fetch("https://randomapi.dev/api/mrz/validate?mrz=PPUTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<%0AL898902C36UTO7408122F1204159ZE184226B<<<<<10");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/mrz/validate?mrz=PPUTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<%0AL898902C36UTO7408122F1204159ZE184226B<<<<<10").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/mrz/validate?mrz=PPUTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<%0AL898902C36UTO7408122F1204159ZE184226B<<<<<10"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 2
mrz string required

The MRZ lines, separated by newlines (%0A) or by '|'. One unbroken 90, 88 or 72-character blob is split by the only geometry it can be. ASCII lower-case letters are upper-cased; anything else stays visible and invalid.

allowed: 1 – 200
example: mrz=PPUTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<|L898902C36UTO7408122F1204159ZE184226B<<<<<10
type enum

MRZ document format. td1 = 3 lines of 30, td2 = 2 lines of 36, td3 = 2 lines of 44 (the passport layout), mrva = 2 lines of 44 and mrvb = 2 lines of 36 (machine readable visas). Omit to detect the format from the line count, the line length and the leading V of a visa.

allowed: td1 | td2 | td3 | mrva | mrvb
example: type=td3
Universal parameters Shared response and formatting options 5
locale enum

Locale for the generated data (names, addresses, …).

default: en
allowed: en | da | de | fr | es | it | pt | ja
example: locale=da
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=input,normalized
exclude list

Return all fields except these (comma-separated).

example: exclude=reasons
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 13
input string

The MRZ exactly as supplied.

example: ppUTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<|L898902C36UTO7408122F1204159ZE184226B<<<<<10

normalized string

Upper-cased input with the supplied separators replaced by newlines and each line trimmed.

example: PPUTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<< L898902C36UTO7408122F1204159ZE184226B<<<<<10

type string nullable

Resolved MRZ format — the requested type, or the one detected from line count, line length and the leading V of a visa. Null when no format matches.

example: td3

lines string[]

The normalized machine readable lines.

example: ["PPUTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<","L898902C36UTO7408122F1204159ZE184226B<<<<<10"]

lineLengths integer[]

Character count of each supplied line.

example: [44,44]

expectedLineCount integer nullable

Line count the resolved format requires; null when no format matched.

example: 2

expectedLineLength integer nullable

Line length the resolved format requires; null when no format matched.

example: 44

geometryValid boolean

Whether line count and every line length match the resolved format exactly.

example: true

characterSetValid boolean

Whether every character is inside the MRZ alphabet A-Z, 0-9 and <. Vacuously true for empty input, which is reported as EMPTY_INPUT instead.

example: true

parsed object nullable

Decoded MRZ fields (document code, state, number, nationality, raw YYMMDD dates, sex, optional data and the identifiers). Null when the geometry does not match.

example: {"documentCode":"PP","issuingState":"UTO","documentNumber":"L898902C3","documentNumberCheckDigit":"6","nationality":"UTO","dateOfBirth":"740812","dateOfBirthCheckDigit":"2","sex":"F","dateOfExpiry":"120415","dateOfExpiryCheckDigit":"9","optionalData":["ZE184226B<<<<<"],"optionalDataCheckDigit":"1","compositeCheckDigit":"0","nameField":"ERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<","primaryIdentifier":"ERIKSSON","secondaryIdentifier":"ANNA MARIA"}

checkDigits object[]

One ordered entry per 7-3-1 check digit: field, the positions it covers, the recomputed expected digit, the digit found and whether they agree. Empty when the geometry does not match.

example: [{"field":"documentNumber","covers":"line 2, positions 1-9","expected":"6","found":"6","valid":true}]

valid boolean

True only when geometry, alphabet, sex, date fields and every check digit are correct. It is not an authenticity or identity check.

example: true

reasons string[]

Stable ordered reason codes; empty exactly when valid is true.

example: []

Documented examples

Build-generated requests and complete responses
5
The ICAO Doc 9303 passport specimen
GET /api/mrz/validate?mrz=PPUTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<%0AL898902C36UTO7408122F1204159ZE184226B<<<<<10
{
  "data": {
    "input": "PPUTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<\nL898902C36UTO7408122F1204159ZE184226B<<<<<10",
    "normalized": "PPUTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<\nL898902C36UTO7408122F1204159ZE184226B<<<<<10",
    "type": "td3",
    "lines": [
      "PPUTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<",
      "L898902C36UTO7408122F1204159ZE184226B<<<<<10"
    ],
    "lineLengths": [
      44,
      44
    ],
    "expectedLineCount": 2,
    "expectedLineLength": 44,
    "geometryValid": true,
    "characterSetValid": true,
    "parsed": {
      "documentCode": "PP",
      "issuingState": "UTO",
      "documentNumber": "L898902C3",
      "documentNumberCheckDigit": "6",
      "nationality": "UTO",
      "dateOfBirth": "740812",
      "dateOfBirthCheckDigit": "2",
      "sex": "F",
      "dateOfExpiry": "120415",
      "dateOfExpiryCheckDigit": "9",
      "optionalData": [
        "ZE184226B<<<<<"
      ],
      "optionalDataCheckDigit": "1",
      "compositeCheckDigit": "0",
      "nameField": "ERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<",
      "primaryIdentifier": "ERIKSSON",
      "secondaryIdentifier": "ANNA MARIA"
    },
    "checkDigits": [
      {
        "field": "documentNumber",
    …
    "generatedAt": "2026-07-29T08:13:22.000Z"
  }
}
A corrupted document-number check digit
GET /api/mrz/validate?mrz=PPUTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<%0AL898902C35UTO7408122F1204159ZE184226B<<<<<10
{
  "data": {
    "input": "PPUTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<\nL898902C35UTO7408122F1204159ZE184226B<<<<<10",
    "normalized": "PPUTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<\nL898902C35UTO7408122F1204159ZE184226B<<<<<10",
    "type": "td3",
    "lines": [
      "PPUTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<",
      "L898902C35UTO7408122F1204159ZE184226B<<<<<10"
    ],
    "lineLengths": [
      44,
      44
    ],
    "expectedLineCount": 2,
    "expectedLineLength": 44,
    "geometryValid": true,
    "characterSetValid": true,
    "parsed": {
      "documentCode": "PP",
      "issuingState": "UTO",
      "documentNumber": "L898902C3",
      "documentNumberCheckDigit": "5",
      "nationality": "UTO",
      "dateOfBirth": "740812",
      "dateOfBirthCheckDigit": "2",
      "sex": "F",
      "dateOfExpiry": "120415",
      "dateOfExpiryCheckDigit": "9",
      "optionalData": [
        "ZE184226B<<<<<"
      ],
      "optionalDataCheckDigit": "1",
      "compositeCheckDigit": "0",
      "nameField": "ERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<",
      "primaryIdentifier": "ERIKSSON",
      "secondaryIdentifier": "ANNA MARIA"
    },
    "checkDigits": [
      {
        "field": "documentNumber",
    …
    "generatedAt": "2026-07-29T08:13:22.000Z"
  }
}
The ICAO TD1 specimen, detected from its geometry
GET /api/mrz/validate?mrz=I<UTOD231458907<<<<<<<<<<<<<<<%0A7408122F1204159UTO<<<<<<<<<<<6%0AERIKSSON<<ANNA<MARIA<<<<<<<<<<
{
  "data": {
    "input": "I<UTOD231458907<<<<<<<<<<<<<<<\n7408122F1204159UTO<<<<<<<<<<<6\nERIKSSON<<ANNA<MARIA<<<<<<<<<<",
    "normalized": "I<UTOD231458907<<<<<<<<<<<<<<<\n7408122F1204159UTO<<<<<<<<<<<6\nERIKSSON<<ANNA<MARIA<<<<<<<<<<",
    "type": "td1",
    "lines": [
      "I<UTOD231458907<<<<<<<<<<<<<<<",
      "7408122F1204159UTO<<<<<<<<<<<6",
      "ERIKSSON<<ANNA<MARIA<<<<<<<<<<"
    ],
    "lineLengths": [
      30,
      30,
      30
    ],
    "expectedLineCount": 3,
    "expectedLineLength": 30,
    "geometryValid": true,
    "characterSetValid": true,
    "parsed": {
      "documentCode": "I<",
      "issuingState": "UTO",
      "documentNumber": "D23145890",
      "documentNumberCheckDigit": "7",
      "nationality": "UTO",
      "dateOfBirth": "740812",
      "dateOfBirthCheckDigit": "2",
      "sex": "F",
      "dateOfExpiry": "120415",
      "dateOfExpiryCheckDigit": "9",
      "optionalData": [
        "<<<<<<<<<<<<<<<",
        "<<<<<<<<<<<"
      ],
      "optionalDataCheckDigit": null,
      "compositeCheckDigit": "6",
      "nameField": "ERIKSSON<<ANNA<MARIA<<<<<<<<<<",
      "primaryIdentifier": "ERIKSSON",
      "secondaryIdentifier": "ANNA MARIA"
    },
    …
    "generatedAt": "2026-07-29T08:13:22.000Z"
  }
}
A visa MRZ, which has no composite check digit
GET /api/mrz/validate?mrz=V<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<%0AL8988901C4XXX7408122F96121096ZE184226B<<<<<<
{
  "data": {
    "input": "V<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<\nL8988901C4XXX7408122F96121096ZE184226B<<<<<<",
    "normalized": "V<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<\nL8988901C4XXX7408122F96121096ZE184226B<<<<<<",
    "type": "mrva",
    "lines": [
      "V<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<",
      "L8988901C4XXX7408122F96121096ZE184226B<<<<<<"
    ],
    "lineLengths": [
      44,
      44
    ],
    "expectedLineCount": 2,
    "expectedLineLength": 44,
    "geometryValid": true,
    "characterSetValid": true,
    "parsed": {
      "documentCode": "V<",
      "issuingState": "UTO",
      "documentNumber": "L8988901C",
      "documentNumberCheckDigit": "4",
      "nationality": "XXX",
      "dateOfBirth": "740812",
      "dateOfBirthCheckDigit": "2",
      "sex": "F",
      "dateOfExpiry": "961210",
      "dateOfExpiryCheckDigit": "9",
      "optionalData": [
        "6ZE184226B<<<<<<"
      ],
      "optionalDataCheckDigit": null,
      "compositeCheckDigit": null,
      "nameField": "ERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<",
      "primaryIdentifier": "ERIKSSON",
      "secondaryIdentifier": "ANNA MARIA"
    },
    "checkDigits": [
      {
        "field": "documentNumber",
    …
    "generatedAt": "2026-07-29T08:13:22.000Z"
  }
}
Wrong line length reported instead of a parse
GET /api/mrz/validate?mrz=PPUTOERIKSSON<<ANNA<MARIA%0AL898902C36UTO7408122F1204159&type=td3
{
  "data": {
    "input": "PPUTOERIKSSON<<ANNA<MARIA\nL898902C36UTO7408122F1204159",
    "normalized": "PPUTOERIKSSON<<ANNA<MARIA\nL898902C36UTO7408122F1204159",
    "type": "td3",
    "lines": [
      "PPUTOERIKSSON<<ANNA<MARIA",
      "L898902C36UTO7408122F1204159"
    ],
    "lineLengths": [
      25,
      28
    ],
    "expectedLineCount": 2,
    "expectedLineLength": 44,
    "geometryValid": false,
    "characterSetValid": true,
    "parsed": null,
    "checkDigits": [],
    "valid": false,
    "reasons": [
      "WRONG_LINE_LENGTH"
    ]
  },
  "meta": {
    "endpoint": "mrz",
    "route": "/validate",
    "locale": "en",
    "params": {
      "mrz": "PPUTOERIKSSON<<ANNA<MARIA\nL898902C36UTO7408122F1204159",
      "type": "td3"
    },
    "generatedAt": "2026-07-29T08:13:22.000Z"
  }
}

About this API

Coverage & behavior

Machine readable zone test fixtures for passport scanners, MRZ OCR pipelines and KYC ingest parsers — so you can exercise a reader without photographing a real passport. Every line is built from the ICAO Doc 9303 layout tables and the 7-3-1 weighted modulus-10 check digit of Doc 9303-3 Section 4.9.

What this is not. This endpoint implements MRZ layout and check-digit arithmetic only. There is no contactless chip, no BAC, PACE or EAC key derivation, no LDS data groups, no country signing certificate, no digital signature, no authenticity check and no identity claim of any kind. Nothing here asserts that a person, a document or a number exists. A correct check digit only means the surrounding characters are internally consistent.

Every document is a specimen, by construction. The issuing State and the nationality are hard-locked to UTO — "Utopia", the code Doc 9303-3 Section 5 Part G recommends for specimen documents — and the document number is always the literal word SPECIMEN followed by one digit. Utopia is not a real State and issues nothing, so no record here is, or can become, a document of any issuing authority. Names come from generic mock name pools, never from real people.

Geometry is exact. TD1 is 3 lines of 30 characters, TD2 is 2 lines of 36, TD3 (the passport data page) is 2 lines of 44, MRV-A is 2 lines of 44 and MRV-B is 2 lines of 36. Unused positions carry the filler <. Check digits cover the document number, the date of birth, the date of expiry, the TD3 personal-number field, and the composite digit whose covered substrings differ per format — TD1 covers line 1 positions 6-30 plus line 2 positions 1-7, 9-15 and 19-29; TD2 covers line 2 positions 1-10, 14-20 and 22-35; TD3 covers line 2 positions 1-10, 14-20 and 22-43. Machine readable visas have no composite check digit at all, so checkDigits.composite is null for mrva and mrvb.

Names follow Doc 9303-3 Section 4.6. Components are separated by one filler, the primary and secondary identifiers by two. Apostrophes are removed and the parts joined (O'CONNOROCONNOR); hyphens become a single filler (SMITH-JONESSMITH<JONES); digits and all other punctuation are dropped. National characters are transliterated with Doc 9303-3 Section 6.A, the Latin table, always taking the first listed expansion — ÄAE, ÅAA, ÖOE, ØOE, ÜUE, ÑN, ÞTH, SS — so one input name always produces exactly one MRZ. The Cyrillic and Arabic tables are not implemented, so a name whose identifiers leave no A–Z characters after transliteration is redrawn from the English name pool and reported in meta.warnings — which is exactly what locale=ja does, since Japanese script has no Doc 9303 Latin transliteration.

Roughly one generated record in eight has no secondary identifier at all — the ARKFREITH<<<<<<… shape of Doc 9303-4 Section 4.2.3.1 (f) and Doc 9303-5 Section 4.2.3.5, whose name field contains no << break at all. That is a distinct parse, so the fixtures include it rather than leaving your reader to meet it in production; secondaryIdentifier is an empty string for those records.

Parameters that genuinely constrain the output. type picks the format and therefore the line count and length. sex writes M, F or — for sex=X — the filler < that Doc 9303 requires in the MRZ when the printed page shows X. minAge/maxAge bound ageYears at the asOf anchor, and expired forces the expiry strictly before or strictly after it; omit expired for a realistic mix. longName=true forces the truncation path: the name field is filled to the last position and, as Doc 9303-4 Section 4.2.3 requires, ends with a letter. longName=false is the natural case, but a naturally long name can still overflow a name field — most often the 30 and 31-character TD1, TD2 and MRV-B ones, occasionally even the 39-character TD3 and MRV-A fields — so nameTruncated always reports the truth rather than promising something the data cannot keep.

One truncation strategy, chosen and stated. Doc 9303 lets each issuing State decide how to shorten an over-long name, and its own examples show three different choices. This endpoint always makes the minimal cut: characters are removed from the end of the assembled field, and the primary identifier is shortened only when it leaves no room for << plus the first character of the secondary identifier. Component separators always survive the cut — Doc 9303-3 Section 4.6 requires a single filler between components, and no published example ever merges two of them — so when a cut lands on a separator, the component in front of it gives up its last character and the freed final position carries the following component's initial (…<ANTONI<L, the shape of …<DEJTHAMRONG<K in Doc 9303-4 Section 4.2.3.2 (a)). The field therefore still ends with A–Z, and every component in it is a genuine prefix of the corresponding component of the full name. That rule reproduces the published Doc 9303-4 Section 4.2.3.2 (a) and (b) truncated examples, and the truncated TD1 example in Doc 9303-5 Section 4.2.3.4, character for character; the Doc 9303-4 Section 4.2.3.3 examples use a different, equally valid issuer strategy and are deliberately not imitated.

/validate recomputes every check digit of an MRZ you supply and returns an ordered list of reason codes instead of throwing. It accepts newline-separated or |-separated lines, or one unbroken 72/88/90-character blob, and detects the format from the geometry plus the leading V of a visa. It deliberately does not guess a century for the two-digit years, and it does not convert spaces to < — a space is reported as an invalid character rather than silently repaired. Date fields are checked component by component: each of YY, MM and DD must be two digits or two fillers, every component that is written must be a real calendar component (74<<99 is reported, not waved through as "has a filler"), and the filler form is accepted only for the date of birth — Doc 9303-3 Section 4.8 grants it to the date of birth alone, and the date-of-expiry element carries no filler allowance in the Part 5, 6 and 7 data element directories, so <<<<<< there is INVALID_DATE_OF_EXPIRY. Characters outside the MRZ alphabet are counted as zero while a check digit is recomputed, and flagged separately as INVALID_CHARACTERS, so a garbled scan still produces a full report instead of an exception. On TD3 it honours Doc 9303-4's rule that an unused personal-number field may carry either 0 or < as its check digit.

Deliberate exclusions, so nothing here is a surprise. Optional-data and personal-number fields are always all filler in generated documents. The TD1 long document number variant of Doc 9303-5 Section 4.2.4 — where position 15 holds < and the number continues into the optional-data field — is neither generated nor recognised, so /validate reads position 15 as the document-number check digit and reports INVALID_CHECK_DIGIT_DOCUMENT_NUMBER for such a card. /validate checks structure only: it does not verify that an issuing State or nationality code is a real ICAO code, that a document code is a harmonised one, or that a date is plausible beyond being a real calendar date. The Cyrillic and Arabic transliteration tables and reverse transliteration are out of scope, and the endpoint makes no network calls of any kind.

Use it for

  • Feed a passport-scanner or MRZ OCR pipeline valid fixtures without photographing a real document
  • Test a KYC ingest parser against TD1, TD2, TD3, MRV-A and MRV-B geometries in one call
  • Prove a check-digit implementation by mutating a generated MRZ and re-running /validate
  • Exercise the Doc 9303 name-truncation path with longName=true on the shorter TD1 and MRV-B fields
  • Generate expired and not-yet-expired documents to test date handling at a fixed asOf anchor

Frequently asked questions

How is the MRZ check digit calculated?

ICAO Doc 9303-3 Section 4.9 uses modulus 10 with the repeating weighting 7, 3, 1: digits count as themselves, A to Z count as 10 to 35, the filler < counts as zero, and the remainder is the check digit. /api/mrz/validate recomputes every one of them and shows the expected digit next to the digit found.

Are these real passport numbers?

No. Every document is issued by UTO — the fictional state Doc 9303 uses in its own examples — and every document number is the literal word SPECIMEN plus one digit. These are scanner and parser test fixtures; there is no chip, no signature and no identity behind them.

Which MRZ formats are supported?

TD1 (3 lines of 30), TD2 (2 lines of 36), TD3 (2 lines of 44), MRV-A (2 lines of 44) and MRV-B (2 lines of 36). Machine readable visas carry no composite check digit, so checkDigits.composite is null for mrva and mrvb.

Why does my MRZ end with a letter instead of a filler?

Usually because the name was truncated — but not necessarily. Doc 9303-4 Section 4.2.3 requires a shortened name field to be completely filled and to end with A-Z, and a name that happens to fill the field exactly ends with a letter too (PAPANDROPOULOUS<<JONATHON<ALEC), which is why Doc 9303 says such a name must be treated as though it had been truncated. Generated records carry a nameTruncated flag that tells you which case you actually have.

Does the MRZ say which century a date of birth is in?

No — the MRZ carries only YYMMDD, and Doc 9303 defines no century rule. Generated records include the full dateOfBirth, but /validate deliberately returns the raw six digits rather than guessing a century.

Can I put an X in the sex field of the MRZ?

Not in the MRZ. Doc 9303 allows M, F or the filler <; X is what the printed page shows when the sex is unspecified. sex=X therefore writes <, and /validate reports INVALID_SEX for any other character.

Standards & references