Extract Passport Data with n8n and Cryvis
n8n KYC flow: two binary properties → HTTP Request Form-Data first_page + last_page → IF success / MRZ → HubSpot or Sheets.
Passport KYC fails in automation when HTTP Request sends a single file part. Cryvis expects two multipart fields — first_page (biodata) and last_page (final page). This workflow downloads both binaries, posts them to Cryvis, and maps biodata + MRZ into HubSpot (or Sheets).
Hub: /blog/n8n. Multipart variants: HTTP Request OCR. Aadhaar twin: Extract Aadhaar data.
Architecture
Form / Drive / Webhook
email
biodata upload → $binary.first_page (or data)
final upload → $binary.last_page
|
v
IF both binaries exist
|
v
HTTP Request
POST https://api.cryvis.com/v1/documents/passport
Form-Data:
first_page = <biodata binary property>
last_page = <final binary property>
Authorization: Bearer <API_KEY>
|
v
IF {{ $json.success }}
→ IF {{ $json.data.mrz.check_digits_valid }}
→ HubSpot Create/Update contactPrerequisites
- Cryvis API key with passport access — Passport API
- n8n Cloud or self-hosted
- Source that yields two files (Typeform/webhook + downloads, or Drive folder with paired names)
- HubSpot credential (or Sheets if you prefer a queue table)
Credits: 1 per image — two pages ≈ two credits. Credits.
Step 1: Get two binaries
Webhook / form with two uploads: each file lands in $binary under its property name. Rename with a Move Binary Data / Set pattern if needed so properties are explicit: first_page and last_page.
Drive: two Download nodes (biodata file id, final file id). If both default to data, use Move Binary Data (or Code) so one item carries:
$binary.first_page
$binary.last_pageIF before Cryvis: continue only if both binary properties exist. Calls with one part return a client error.
Step 2: HTTP Request passport
| Setting | Value |
|---|---|
| Method | POST |
| URL | https://api.cryvis.com/v1/documents/passport |
| Auth | Authorization: Bearer <API_KEY> |
| Body Content Type | Form-Data |
| Form-Data name | Type | Input Data Field Name |
|---|---|---|
first_page | File | first_page (biodata) |
last_page | File | last_page (final) |
MIME: application/pdf, image/jpeg, image/png, image/webp.
Wrong vs right
# Wrong — not the OpenAPI contract
file = $binary.data
# Right
first_page = biodata binary
last_page = final binaryDocs: extractPassport.
Step 3: Response and expressions
{
"success": true,
"data": {
"full_name": "JOHN DOE",
"given_name": "JOHN",
"surname": "DOE",
"date_of_birth": "1995-04-12",
"nationality": "IND",
"passport_number": "P1234567",
"issue_date": "2020-04-12",
"expiry_date": "2030-04-12",
"issuing_country": "IND",
"sex": "M",
"mrz": {
"line1": "P<INDDOE<<JOHN<<<<<<<<<<<<<<<<<<<<<<<<<<<<",
"line2": "P1234567<3IND9504124M3004123<<<<<<<<<<<<<<<4",
"check_digits_valid": true
}
},
"meta": {}
}| Use | Expression |
|---|---|
| Full name | {{ $json.data.full_name }} |
| Given / surname | {{ $json.data.given_name }} / {{ $json.data.surname }} |
| DOB | {{ $json.data.date_of_birth }} |
| Nationality | {{ $json.data.nationality }} |
| Passport # | {{ $json.data.passport_number }} |
| Issue / expiry | {{ $json.data.issue_date }} / {{ $json.data.expiry_date }} |
| Issuing country | {{ $json.data.issuing_country }} |
| Sex | {{ $json.data.sex }} |
| MRZ OK | {{ $json.data.mrz.check_digits_valid }} |
| MRZ lines | {{ $json.data.mrz.line1 }}, {{ $json.data.mrz.line2 }} |
Fields can be null on bad scans. Guard CRM updates so you do not overwrite good contact data with blanks — IF empty checks or Set node defaults.
Step 4: HubSpot (or Sheets) + Switch on MRZ
Switch (or nested IF):
{{ $json.data.mrz.check_digits_valid }} equals true
→ kyc_status = passport_extracted
false
→ kyc_status = passport_mrz_reviewOptional expiry gate (IF / Code): if expiry_date is within 30 days or past, set passport_expiring_or_expired. Cryvis returns the date string; it does not enforce your policy.
HubSpot custom properties (once): passport_number, passport_nationality, passport_issue_date, passport_expiry_date, passport_issuing_country, passport_mrz_valid, kyc_status.
Prefer storing raw MRZ lines in a restricted store, not on every CRM layout — keep passport_mrz_valid boolean on the contact.
Step 5: Errors
| Case | Action |
|---|---|
| 401 | Alert auth failure; stop |
| 400 | Missing first_page/last_page or bad image — ask resubmit |
| 5xx | Retry on Fail (2–3) then Error Trigger → Slack |
Never retry endlessly on 400. Self-hosted: shorten execution retention — passport images are PII.
Testing checklist
- Two clear JPEGs →
success: true, non-nullpassport_number. - Only biodata → IF blocks before HTTP.
- Swapped pages → degraded fields / MRZ — confirm operators labeled uploads correctly.
check_digits_valid: false→ HubSpot review status.
Mapping pitfalls
- Mapping a file URL into Form-Data as text — download to
$binaryfirst. - Using
fileinstead offirst_page/last_page. - Reading
{{ $json.passport_number }}instead of{{ $json.data.passport_number }}. - Overwriting HubSpot DOB with null — guard empty values.
Related
Sign up · Pricing · Passport · extractPassport. POST multipart first_page + last_page to https://api.cryvis.com/v1/documents/passport with Bearer auth.