Identity · Document verification
Read the document the way the issuer wrote it.
KYCVerify locates the machine-readable zone, parses it to the ICAO 9303 layout and recomputes every check digit. Indian cards without an MRZ are read by OCR. Image quality, expiry, country and type are checked before a single field is trusted.
- MRZ formats: TD1, TD2, TD3, MRV-A, MRV-B, French ID
- 6
- document types accepted by a workflow
- 7
- attempts per step by default
- 3
What it checks
Document verification, check by check.
Each rule below is what the engine actually runs. The result is written as a document check with its score and warnings.
Check digits
Document number, date of birth, expiry, optional data and the composite digit, each recomputed with ICAO's 7-3-1 weights. Long TD1/TD2 document numbers that continue into the optional field are handled.
OCR correction you can see
Common confusions (O and 0, I and 1) are corrected only in positions whose alphabet is known, and every correction is reported, so a reviewer can tell a clean read from a repaired one.
Image quality
Resolution, sharpness (variance of the Laplacian), exposure and glare are measured first. A blurry or glare-washed capture is sent back to the person with a reason instead of being guessed at.
Expiry, type and country
An expired document is declined when the workflow says so. The declared type and issuing country must match what the MRZ says, and the country must be on the workflow's allow-list.
Indian cards
Aadhaar, PAN, voter ID and Indian driving licences are read by OCR from the bilingual card. Nothing is invented: a field that cannot be located stays empty and is listed as an issue.
Portrait and fingerprint
The portrait is detected and embedded for face match. The document number is hashed into a fingerprint so a second session with the same document can be flagged.
Try it
Recompute a check digit yourself.
The same 7-3-1 arithmetic the engine runs, on ICAO's own specimens. Edit a character and watch the digit that guards it stop verifying.
- The rules are ported line for line from the Rust engine, and the page names the file.
- Everything runs locally in this tab. No request is made while you type.
- Reset puts the example back; nothing is saved.
MRZ check-digit calculator
One line per row: 2 x 44 (passport, MRV-A visa), 2 x 36 (TD2, MRV-B) or 3 x 30 (ID card). The specimens are ICAO's own.
P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<< L898902C36UTO7408122F1204159ZE184226B<<<<<10
TD3: every check digit verifies
Check digits
Working: document number
| char | L | 8 | 9 | 8 | 9 | 0 | 2 | C | 3 |
|---|---|---|---|---|---|---|---|---|---|
| value | 21 | 8 | 9 | 8 | 9 | 0 | 2 | 12 | 3 |
| weight | 7 | 3 | 1 | 7 | 3 | 1 | 7 | 3 | 1 |
| product | 147 | 24 | 9 | 56 | 27 | 0 | 14 | 36 | 3 |
sum = 316 · 316 mod 10 = 6 · printed 6
Parsed fields
- Document code
- P
- Issuing state
- UTO
- Surname
- ERIKSSON
- Given names
- ANNA MARIA
- Document number
- L898902C3
- Nationality
- UTO
- Date of birth
- 1974-08-12
- Sex
- F
- Date of expiry
- 2012-04-15
Logic ported from backend/crates/kyc-mrz/src/lib.rs. Nothing you type leaves this page.
How it works
What happens, in order.
- 1
Capture
The hosted flow asks for the document type and issuing country, then the front (and back where it matters). Uploads are capped at 10 MB, MIME-sniffed and encrypted at rest.
- 2
Quality gate
Sharpness, exposure and glare are measured. A capture that fails is rejected with a retry, without spending the decision.
- 3
Read
OCR runs on the image, the MRZ is found among the noise lines and parsed, or the Indian-card extractor reads the printed fields.
- 4
Verify
Check digits, expiry, type and country are verified, the identity is merged with its source recorded per field, and a
documentcheck is written.
Configuration
The workflow keys and their defaults.
"document": {
"enabled": true,
"allowed_types": ["passport", "id_card", "driving_licence",
"residence_permit", "aadhaar", "pan", "voter_id"],
"allowed_countries": [],
"reject_expired": true,
"max_attempts": 3
}| Key | Default | Meaning |
|---|---|---|
| allowed_types | all 7 | Which document types the person may choose. |
| allowed_countries | [] (any) | ISO 3166-1 alpha-3 issuing countries, e.g. IND, GBR. |
| reject_expired | true | Decline a document past its expiry date. |
| max_attempts | 3 | Captures allowed before the step fails. |
Reference
Six MRZ layouts, one parser.
The layout is chosen from the line count and length. Positions are fixed by ICAO Doc 9303, so every field and its check digit sit in a known place.
| Format | Lines | Used on | Check digits |
|---|---|---|---|
| TD3 | 2 x 44 | Passports | Number, birth, expiry, personal number, composite |
| TD2 | 2 x 36 | ID cards, some residence permits | Number (long form), birth, expiry, composite |
| TD1 | 3 x 30 | ID cards, residence permits | Number (long form), birth, expiry, composite |
| MRV-A | 2 x 44 | Visas, full-page | Number, birth, expiry |
| MRV-B | 2 x 36 | Visas, smaller format | Number, birth, expiry |
| French ID | 2 x 36 | French national ID, 1988-2021 | Number, birth, composite (no expiry in the MRZ) |
Reasons and warnings
Exact codes, as they appear in the check's data and warnings, so you can branch on them.
| Code | Outcome | When |
|---|---|---|
| document_expired | failed | Expiry date is in the past and reject_expired is on (a warning only when it is off). |
| document_country_not_allowed | failed | The MRZ issuing state is not on the workflow's allowed_countries list. |
| document_mrz_invalid | review | One or more check digits do not verify (warning mrz_check_digits_failed). |
| document_mrz_unreadable | review | No machine-readable zone could be read from the image. |
| document_type_mismatch | review | The MRZ document code does not match the type the person chose. |
| document_country_mismatch | review | The declared issuing country differs from the MRZ. |
| document_number_invalid | review | An Aadhaar number read from a card fails its Verhoeff checksum, or a PAN fails its format. |
| document_unreadable | retake | Too small, blurred, too dark or glare-washed: the person is asked to capture again, without spending the decision. |
API
Parse an MRZ from your backend.
POST /v1/checks/mrz takes the 2 or 3 lines (or up to 20 noisy OCR lines, from which the MRZ is found) and returns every field, each check digit and any OCR correction.
In a session
A document check on the ICAO specimen passport
{
"kind": "document",
"status": "failed",
"score": 1.0,
"data": {
"document_type": "passport",
"country": "UTO",
"attempt": 1,
"issuing_country": "UTO",
"verification_level": "mrz_check_digits",
"mrz": {
"format": "td3", "document_code": "P", "issuing_state": "UTO", "valid": true,
"check_digits": { "document_number": true, "birth_date": true, "expiry_date": true,
"optional_data": true, "composite": true },
"corrections": 0
},
"reason": "document_expired"
},
"warnings": [
{ "code": "document_expired", "message": "The document expired on 2012-04-15", "severity": "high" }
]
}Parse an MRZ from your backend.
curl -X POST https://kycverify.me/api/v1/checks/mrz \
-H "x-api-key: $KYC_API_KEY" \
-H "content-type: application/json" \
-d '{
"lines": [
"P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<",
"L898902C36UTO7408122F1204159ZE184226B<<<<<10"
]
}'200 OK (abridged)
{
"format": "td3",
"document_code": "P",
"issuing_state": "UTO",
"surname": "ERIKSSON",
"given_names": "ANNA MARIA",
"document_number": "L898902C3",
"nationality": "UTO",
"birth_date": "1974-08-12",
"sex": "F",
"expiry_date": "2012-04-15",
"check_digits": {
"document_number": true, "birth_date": true, "expiry_date": true,
"optional_data": true, "composite": true
},
"valid": true,
"corrections": []
}Limits
What it does not do.
Stated up front, so you can decide what to pair it with.
- No NFC chip reading: the e-passport chip is not read, so the check relies on the printed MRZ and the image.
- No forensic template matching against a library of every issuer's security features. Authenticity rests on MRZ integrity, consistency and quality.
- Indian cards have no MRZ, so their fields come from OCR and carry lower certainty than a check-digit-verified MRZ.
FAQ
Document verification: questions.
Which documents are supported?
Anything with an ICAO 9303 MRZ (passports, most national ID cards, residence permits, visas) from any country, plus Aadhaar, PAN, voter ID and Indian driving licences read by OCR. A workflow can narrow both the types and the issuing countries.
What happens when a check digit fails?
The parsed fields are kept with valid: false, a mrz_check_digits_failed warning is raised and the document check goes to review as document_mrz_invalid, so a person looks at the image before anything is decided.
Is the full document number stored?
No. The identity keeps a masked number (L8989****). Duplicate detection compares a keyed HMAC-SHA256 fingerprint that is kept internally and never appears in the identity, decisions or webhooks.
Try it in the sandbox today.
Every check is available from the first sign-up, with test keys and a default workflow. Talk to us when you are ready to verify real people.