← Back to blog

Extract Aadhaar Data with n8n: Gmail to Sheets (Masked UID Handling)

n8n: Gmail attachments → Cryvis POST /v1/documents/aadhaar with one or two file parts → IF is_masked → Google Sheets with PII controls.

Aadhaar shows up in inboxes as KYC threads with one or two JPEG attachments. This n8n workflow watches Gmail, sends attachments to Cryvis POST /v1/documents/aadhaar, and writes structured fields to Google Sheets — with explicit handling for is_masked and PII hygiene.

Passport (two differently named parts): Extract passport data. HTTP building block: HTTP Request OCR. Hub: /blog/n8n.

Why is_masked is first-class

Cryvis returns whatever is printed: full UID or masked (e.g. XXXX XXXX 9012) plus is_masked: true|false. Treat that flag as a control for storage policy, not an afterthought. This is engineering guidance, not legal advice — confirm retention with counsel under applicable UIDAI / DPDP rules.

Architecture

Gmail label: kyc-aadhaar
        |
        v
Gmail Trigger → attachment binaries
        |
        v
IF MIME image/pdf; cap at 2 files
        |
        v
HTTP Request
  POST https://api.cryvis.com/v1/documents/aadhaar
  Form-Data: file [, file]
  Authorization: Bearer ...
        |
        v
IF {{ $json.success }}
  → Switch on {{ $json.data.is_masked }}
  → Google Sheets Append

Step 1: Gmail intake

  1. Label kyc-aadhaar via Gmail filter (attachment + subject keywords).
  2. Gmail Trigger filtered by that label.
  3. Instruct senders: front only, back only, or front+back. Cryvis accepts 1–2 files, both named file.

Step 2: Collect binaries

Loop attachments; IF MIME ∈ image/jpeg, image/png, image/webp, application/pdf.

Cap at two. If more arrive, take the first two and Set a note extra_attachments_ignored.

When you need two parts on one item for HTTP Request:

$binary.data       → first attachment
$binary.data_back  → second attachment (optional)

Use Move Binary Data / Code so the second file is not overwritten if both downloads default to data.

Step 3: Cryvis Aadhaar HTTP Request

SettingValue
MethodPOST
URLhttps://api.cryvis.com/v1/documents/aadhaar
AuthAuthorization: Bearer <API_KEY>
Body Content TypeForm-Data
NameTypeInput Data Field NameWhen
fileFiledataAlways
fileFiledata_backIf second attachment exists

Both parts must be named file — not front/back (those are Indian Vehicle RC) and not first_page/last_page (passport).

cURL equivalent:

curl -X POST https://api.cryvis.com/v1/documents/aadhaar \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@aadhaar-front.jpg" \
  -F "file=@aadhaar-back.jpg"

Docs: extractAadhaar · Aadhaar.

Step 4: Response schema

{
  "success": true,
  "data": {
    "aadhaar_number": "1234 5678 9012",
    "full_name": "RAHUL SHARMA",
    "date_of_birth": "1992-08-14",
    "year_of_birth": null,
    "gender": "Male",
    "care_of": "S/O SURESH SHARMA",
    "address": "12 MG Road, Bengaluru, Karnataka 560001",
    "pincode": "560001",
    "is_masked": false
  },
  "meta": {}
}
FieldNotes
aadhaar_numberFull or masked string as printed
date_of_birthMay be null on YOB-only cards
year_of_birthUse when DOB absent
genderMale / Female / Transgender
care_ofGuardian line when present
address / pincodeOften from back image
is_maskedBoolean — drives storage policy

Expressions: {{ $json.data.full_name }}, {{ $json.data.is_masked }}, etc.

Step 5: Sheets columns

ColumnAccess
Email / Thread IDOps
Full nameOps
DOB or YOBOps
GenderOps
Care ofOps
AddressOps
PincodeOps
Aadhaar numberRestricted — KYC role only, or last-4 only
Is maskedOps
KYC statusOps
Gmail linkOps
Processed atOps

Status values: extracted_full, extracted_masked, needs_resubmit, error.

Step 6: Switch on is_masked

Set node fields:

name       = {{ $json.data.full_name }}
dob_or_yob = {{ $json.data.date_of_birth || $json.data.year_of_birth }}
gender     = {{ $json.data.gender }}
aadhaar    = {{ $json.data.aadhaar_number }}
is_masked  = {{ $json.data.is_masked }}

Switch on {{ $json.data.is_masked }}:

PolicyMasked branchUnmasked branch
Accept masked for address proofStatus extracted_masked (terminal)extracted_full
Require full UIDStatus needs_resubmit + Gmail replyextracted_full
Never store full UIDWrite last 4 only via Code/expressionSame

Pick one policy and document it next to the workflow. Do not log full Aadhaar in Slack.

Step 7: Optional Gmail reply

Gmail Send: success acknowledgment without echoing aadhaar_number. Masked + require-full policy → ask for unmasked resubmit only if your legal basis allows.

Step 8: Errors and retention

HTTP Cryvis
  |
  +-- success → Sheets → optional Gmail reply
  |
  +-- failure → Sheets status=error
              → Slack: message id + HTTP status only

Enable Retry on Fail for 5xx only. Error Trigger for auth/node crashes.

Self-hosted / Cloud: execution history may retain attachment binaries. Shorten retention for Aadhaar workflows; archive Gmail threads per your schedule outside n8n.

Testing

  1. Front+back unmasked → is_masked=false, address filled, extracted_full.
  2. Masked front only → is_masked=true, status per policy.
  3. YOB-only card → DOB null, YOB set; Sheets uses YOB.
  4. Three attachments → cap at 2; note field set.

Why two parts share the name file

OpenAPI defines Aadhaar uploads as 1–2 binaries under file. Different from RC (front/back) and passport (first_page/last_page). In n8n Form-Data, add two rows both named file with different Input Data Field Names — do not rename the second part to file2.

Credits

1 credit per image. Front+back = two credits. Prefer that over one blurry combined photo — Credits.

Sign up · Pricing · Aadhaar · extractAadhaar. POST one or two multipart parts named file to https://api.cryvis.com/v1/documents/aadhaar, then Switch on {{ $json.data.is_masked }} before writing Sheets.