← Back to blog

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 contact

Prerequisites

  • 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_page

IF before Cryvis: continue only if both binary properties exist. Calls with one part return a client error.

Step 2: HTTP Request passport

SettingValue
MethodPOST
URLhttps://api.cryvis.com/v1/documents/passport
AuthAuthorization: Bearer <API_KEY>
Body Content TypeForm-Data
Form-Data nameTypeInput Data Field Name
first_pageFilefirst_page (biodata)
last_pageFilelast_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 binary

Docs: 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": {}
}
UseExpression
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_review

Optional 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

CaseAction
401Alert auth failure; stop
400Missing first_page/last_page or bad image — ask resubmit
5xxRetry on Fail (2–3) then Error Trigger → Slack

Never retry endlessly on 400. Self-hosted: shorten execution retention — passport images are PII.

Testing checklist

  1. Two clear JPEGs → success: true, non-null passport_number.
  2. Only biodata → IF blocks before HTTP.
  3. Swapped pages → degraded fields / MRZ — confirm operators labeled uploads correctly.
  4. check_digits_valid: false → HubSpot review status.

Mapping pitfalls

  1. Mapping a file URL into Form-Data as text — download to $binary first.
  2. Using file instead of first_page / last_page.
  3. Reading {{ $json.passport_number }} instead of {{ $json.data.passport_number }}.
  4. Overwriting HubSpot DOB with null — guard empty values.

Sign up · Pricing · Passport · extractPassport. POST multipart first_page + last_page to https://api.cryvis.com/v1/documents/passport with Bearer auth.