AutomationFlowsAI & RAG › Extract Passport Data with Openai OCR and Generate Qr Codes

Extract Passport Data with Openai OCR and Generate Qr Codes

ByICTS Automation @ictsautomation on n8n.io

This workflow processes passport images submitted through a form, extracts structured data using OpenAI OCR, and generates QR codes with the extracted information. Results are displayed on the form completion page and sent via email.

Event trigger★★★★☆ complexity19 nodesForm TriggerHTTP RequestEdit ImageGmailForm
AI & RAG Trigger: Event Nodes: 19 Complexity: ★★★★☆ Added:
Extract Passport Data with Openai OCR and Generate Qr Codes — n8n workflow card showing Form Trigger, HTTP Request, Edit Image integration

This workflow corresponds to n8n.io template #12868 — we link there as the canonical source.

This workflow follows the Editimage → HTTP Request recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "eWjsQc7LACXBwmYC",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Extract passport data with OpenAI and create QR codes",
  "tags": [],
  "nodes": [
    {
      "id": "ac9f02e6-b5d9-4c92-93e7-818f970ae1fd",
      "name": "On form submission",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        -1360,
        368
      ],
      "parameters": {
        "options": {},
        "formTitle": "Process passport images",
        "formFields": {
          "values": [
            {
              "fieldType": "file",
              "fieldLabel": "Passport images",
              "requiredField": true
            }
          ]
        },
        "formDescription": "Upload multiple passport images and generate QR codes"
      },
      "typeVersion": 2.2
    },
    {
      "id": "1da0691b-96a2-447e-9a7f-38c22321c148",
      "name": "Loop Over images",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -832,
        368
      ],
      "parameters": {
        "options": {
          "reset": "={{ $prevNode.name == 'On form submission'}}"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "5e604ffc-ca95-487e-989c-f7041b5d4dfa",
      "name": "OCR - openAI",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        352,
        368
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/responses",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"model\": \"gpt-4.1-mini\", \n  \"input\": [\n    {\n      \"role\": \"user\",\n      \"content\": [\n        {\n          \"type\": \"input_text\", \n          \"text\": \"If the image is not a normal passport, the result will be returned as empty. If normal passport, extract all passport fields as json encode. Use key: type, surname, given_names, nationality, date_of_birth, sex, place_of_birth, date_of_issue, date_of_expiration, cmnd, issuing_authority, issuing_country, overseas_address, passport_no.\"},\n        {\n          \"type\": \"input_image\",\n          \"image_url\": \"data:image/png;base64,{{ $json.imageBase64 }}\"\n        }\n      ]\n    }\n  ]\n}\n",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "e242bd79-b027-4124-9bd5-4368c2917951",
      "name": "If is image",
      "type": "n8n-nodes-base.if",
      "position": [
        -384,
        384
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "06912eb7-406c-4568-9cfa-2a695cb0f63a",
              "operator": {
                "type": "string",
                "operation": "regex"
              },
              "leftValue": "={{ $('Loop Over images').item.binary.data.fileExtension.toLowerCase() }}",
              "rightValue": "\\.?(jpg|jpeg|png|gif|bmp|webp|svg|tiff|heic|jfif)$"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "5591e2c6-8e40-4d6e-b4fb-60c1c52b97d3",
      "name": "If has result",
      "type": "n8n-nodes-base.if",
      "disabled": true,
      "position": [
        720,
        368
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "5902cd89-732a-4f32-918a-015b90002cda",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{$json.fullname}}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "a0084d12-2a31-4f49-83b7-caf290318d18",
      "name": "Resize Image",
      "type": "n8n-nodes-base.editImage",
      "onError": "continueRegularOutput",
      "position": [
        -144,
        368
      ],
      "parameters": {
        "width": 1000,
        "height": 1000,
        "options": {},
        "operation": "resize"
      },
      "typeVersion": 1
    },
    {
      "id": "6d73bc19-9b0c-4ab0-b3e4-e51daa7c91ea",
      "name": "Update imageBase64",
      "type": "n8n-nodes-base.code",
      "onError": "continueRegularOutput",
      "position": [
        96,
        368
      ],
      "parameters": {
        "jsCode": "let x = $('Loop Over images').first();\nconsole.log('x...', x)\nconsole.log('input...', $input.first())\nconst binBuffer = await this.helpers.getBinaryDataBuffer(0, 'data');\nconsole.log('binBuffer...', binBuffer)\nx.json.imageBase64 = Buffer.from(binBuffer).toString('base64')\nx.binary = $input.first().binary\nreturn x"
      },
      "typeVersion": 2
    },
    {
      "id": "83acad24-214a-4a8a-894b-5818276dd1e1",
      "name": "Data standardization",
      "type": "n8n-nodes-base.code",
      "onError": "continueRegularOutput",
      "position": [
        544,
        368
      ],
      "parameters": {
        "jsCode": "function toTelex(str) {\n  const map = {\n    '\u00e0':'af','\u00e1':'as','\u1ea3':'ar','\u00e3':'ax','\u1ea1':'aj',\n    '\u00e2':'aa','\u1ea7':'aaf','\u1ea5':'aas','\u1ea9':'aar','\u1eab':'aax','\u1ead':'aaj',\n    '\u0103':'aw','\u1eb1':'awf','\u1eaf':'aws','\u1eb3':'awr','\u1eb5':'awx','\u1eb7':'awj',\n    '\u00c0':'Af','\u00c1':'As','\u1ea2':'Ar','\u00c3':'Ax','\u1ea0':'Aj',\n    '\u00c2':'Aa','\u1ea6':'Aaf','\u1ea4':'Aas','\u1ea8':'Aar','\u1eaa':'Aax','\u1eac':'Aaj',\n    '\u0102':'Aw','\u1eb0':'Awf','\u1eae':'Aws','\u1eb2':'Awr','\u1eb4':'Awx','\u1eb6':'Awj',\n    '\u00e8':'ef','\u00e9':'es','\u1ebb':'er','\u1ebd':'ex','\u1eb9':'ej',\n    '\u00ea':'ee','\u1ec1':'eef','\u1ebf':'ees','\u1ec3':'eer','\u1ec5':'eex','\u1ec7':'eej',\n    '\u00c8':'Ef','\u00c9':'Es','\u1eba':'Er','\u1ebc':'Ex','\u1eb8':'Ej',\n    '\u00ca':'Ee','\u1ec0':'Eef','\u1ebe':'Ees','\u1ec2':'Eer','\u1ec4':'Eex','\u1ec6':'Eej',\n    '\u00ec':'if','\u00ed':'is','\u1ec9':'ir','\u0129':'ix','\u1ecb':'ij',\n    '\u00cc':'If','\u00cd':'Is','\u1ec8':'Ir','\u0128':'Ix','\u1eca':'Ij',\n    '\u00f2':'of','\u00f3':'os','\u1ecf':'or','\u00f5':'ox','\u1ecd':'oj',\n    '\u00f4':'oo','\u1ed3':'oof','\u1ed1':'oos','\u1ed5':'oor','\u1ed7':'oox','\u1ed9':'ooj',\n    '\u01a1':'ow','\u1edd':'owf','\u1edb':'ows','\u1edf':'owr','\u1ee1':'owx','\u1ee3':'owj',\n    '\u00d2':'Of','\u00d3':'Os','\u1ece':'Or','\u00d5':'Ox','\u1ecc':'Oj',\n    '\u00d4':'Oo','\u1ed2':'Oof','\u1ed0':'Oos','\u1ed4':'Oor','\u1ed6':'Oox','\u1ed8':'Ooj',\n    '\u01a0':'Ow','\u1edc':'Owf','\u1eda':'Ows','\u1ede':'Owr','\u1ee0':'Owx','\u1ee2':'Owj',\n    '\u00f9':'uf','\u00fa':'us','\u1ee7':'ur','\u0169':'ux','\u1ee5':'uj',\n    '\u01b0':'uw','\u1eeb':'uwf','\u1ee9':'uws','\u1eed':'uwr','\u1eef':'uwx','\u1ef1':'uwj',\n    '\u00d9':'Uf','\u00da':'Us','\u1ee6':'Ur','\u0168':'Ux','\u1ee4':'Uj',\n    '\u01af':'Uw','\u1eea':'Uwf','\u1ee8':'Uws','\u1eec':'Uwr','\u1eee':'Uwx','\u1ef0':'Uwj',\n    '\u1ef3':'yf','\u00fd':'ys','\u1ef7':'yr','\u1ef9':'yx','\u1ef5':'yj',\n    '\u1ef2':'Yf','\u00dd':'Ys','\u1ef6':'Yr','\u1ef8':'Yx','\u1ef4':'Yj',\n    '\u0111':'dd','\u0110':'Dd'\n  };\n\n  return str.split('').map(ch => map[ch] || ch).join('');\n}\n\nfunction isNumeric(str) {\n  return /^[0-9]+$/.test(str);\n}\n\nfunction convertDate(date) {\n  if (date.split('/').length == 3) {\n    const [day, month, year] = date.split(\"/\").map(Number);\n    return new Date(year, month - 1, day);\n  }\n  return new Date(date)\n}\n\nfunction convertVNDate(date) {\n  let ngay = date.getDate()\n  if (ngay < 10) {\n    ngay = '0' + ngay\n  }\n  let thang = date.getMonth() + 1\n  if (thang < 10) {\n    thang = '0' + thang\n  }\n  let nam = date.getFullYear()\n  return ngay + '/' + thang + '/' + nam\n}\nconst objArr = []\nfor (const item of $input.all()) {\n  let output = item.json.output[0].content[0].text\n  console.log(output)\n  if (output.length == 0) {\n    objArr.push({})\n    continue\n  }\n  const output2 = output.match(/\\{[\\s\\S]*\\}/);\n  if (!output2) {\n    objArr.push({})\n    continue\n  }\n  const result = JSON.parse(output2[0])\n  if (!result.type || !result.given_names) {\n    objArr.push({})\n    continue\n  }\n\n  const vnList = ['VN', 'VNM', 'VIETNAM', 'VIET NAM']\n  if (!vnList.includes(result.issuing_country.toUpperCase())) {\n    if (!result.type || !result.given_names) {\n      objArr.push({})\n      continue\n    }\n  }\n  \n  result['date_of_birth'] = convertDate(result['date_of_birth'])\n  result['date_of_issue'] = convertDate(result['date_of_issue'])\n  result['date_of_expiration'] = convertDate(result['date_of_expiration'])\n  \n  result['ngay_sinh'] = result['date_of_birth'].getDate()\n  if (result['ngay_sinh'] < 10) {\n    result['ngay_sinh'] = '0' + result['ngay_sinh']\n  }\n  result['thang_sinh'] = result['date_of_birth'].getMonth() + 1\n  if (result['thang_sinh'] < 10) {\n    result['thang_sinh'] = '0' + result['thang_sinh']\n  }\n  result['nam_sinh'] = result['date_of_birth'].getFullYear()\n  \n  result['ngay_cap'] = result['date_of_issue'].getDate()\n  if (result['ngay_cap'] < 10) {\n    result['ngay_cap'] = '0' + result['ngay_cap']\n  }\n  result['thang_cap'] = result['date_of_issue'].getMonth() + 1\n  if (result['thang_cap'] < 10) {\n    result['thang_cap'] = '0' + result['thang_cap']\n  }\n  result['nam_cap'] = result['date_of_issue'].getFullYear()\n  \n  result['ngay_thang_nam_sinh'] = convertVNDate(result['date_of_birth'])\n  result['ngay_thang_nam_het_han'] = convertVNDate(result['date_of_expiration'])\n  result['ngay_thang_nam_cap'] = convertVNDate(result['date_of_issue'])\n  result['passport_number'] = result['passport_no']\n  result['issue_date'] = result['ngay_thang_nam_cap']\n  result['expiry_date'] = result['ngay_thang_nam_het_han']\n  result['issuing_place'] = toTelex(result['issuing_authority'])\n  result['birth_date'] = result['ngay_thang_nam_sinh']\n  result['birth_place'] = result['place_of_birth']\n  result['domestic_address'] = toTelex(result['place_of_birth'])\n  result['overseas_address'] = ''\n  result['original_nationality'] = 'Viet Nam'\n  result['occupation'] = ''\n  if (result['sex'] == 'M') {\n    result['sex'] = 'Male'\n  }\n  if (result['sex'] == 'F') {\n    result['sex'] = 'Female'\n  }\n  \n  result['sex2'] = result['sex'].toLowerCase()\n  result['fullname'] = result['surname'] + ' ' + result['given_names']\n  result['surname_only'] = result['surname']\n  result['fullname_telex'] = toTelex(result['fullname'])\n  if (!isNumeric(result['cmnd'])) {\n    result['cmnd'] = ''\n  }\n  result['passport_status'] = 'Valid'\n  if (result['date_of_expiration'] < new Date()) {\n    result['passport_status'] = 'Expired'\n  }\n  let nextYear = new Date();\n  nextYear.setFullYear(nextYear.getFullYear() + 1);\n\n  if (result['date_of_expiration'] > nextYear) {\n    result['passport_status'] = 'Lost'\n  }\n  objArr.push({\n    json: result\n  })\n}\nreturn objArr\n"
      },
      "typeVersion": 2
    },
    {
      "id": "fce4c52a-e6ae-444a-96e6-fd4c2126a14b",
      "name": "Send a message",
      "type": "n8n-nodes-base.gmail",
      "position": [
        -96,
        -160
      ],
      "parameters": {
        "sendTo": "YOUR_EMAIL",
        "message": "={{ $json.output }}",
        "options": {},
        "subject": "=[QR] Passport processing results - {{ $now.format('yyyy-MM-dd hh-mm') }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "5b41a7d1-5710-4539-b422-c1909c4c066c",
      "name": "Form",
      "type": "n8n-nodes-base.form",
      "position": [
        -96,
        16
      ],
      "parameters": {
        "options": {},
        "operation": "completion",
        "completionTitle": "Success!",
        "completionMessage": "={{$json.output}}"
      },
      "typeVersion": 1
    },
    {
      "id": "3c7b8c9b-a7a4-493a-a787-99d5c2b974be",
      "name": "Grab image",
      "type": "n8n-nodes-base.code",
      "position": [
        -352,
        16
      ],
      "parameters": {
        "jsCode": "let output = ''\nfor (const item of $input.all()) {\n  if (item.json.url) {\n    output = output + `[Passport] ${item.json.name} <br/><img src=\"${item.json.url}\"/><br/>`\n  }\n}\n\nreturn {output}"
      },
      "typeVersion": 2
    },
    {
      "id": "416bc7b1-dca0-4c22-87b0-18d718037aa1",
      "name": "Prepare form",
      "type": "n8n-nodes-base.code",
      "position": [
        -1104,
        368
      ],
      "parameters": {
        "jsCode": "const input = items[0].binary;\nconst output = [];\n\nfor (const key in input) {\n  output.push({\n    binary: {\n      data: input[key]\n    },\n    json: {\n      fileName: input[key].fileName,\n      mimeType: input[key].mimeType\n    }\n  });\n}\n\nreturn output;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "29151a3e-aa10-4ef7-8208-6d38e9e2f215",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -432,
        -272
      ],
      "parameters": {
        "color": 7,
        "width": 688,
        "height": 448,
        "content": "## Result aggregation\nCombine extracted data and QR codes, then deliver via form completion page and email"
      },
      "typeVersion": 1
    },
    {
      "id": "6ddf5763-45d9-4df5-bce3-017087435fbe",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -432,
        240
      ],
      "parameters": {
        "color": 7,
        "width": 688,
        "height": 368,
        "content": "## Image preprocessing\nValidate image files, resize for optimal OCR accuracy, prepare for data extraction"
      },
      "typeVersion": 1
    },
    {
      "id": "28ff2de8-af39-42cb-8f44-ae6a0842d25f",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1408,
        240
      ],
      "parameters": {
        "color": 7,
        "width": 784,
        "height": 368,
        "content": "## Form input processing\nReceive multiple image uploads from form, split into individual items for sequential processing"
      },
      "typeVersion": 1
    },
    {
      "id": "60da4d75-335a-4375-81d2-80367b48eea4",
      "name": "Prepare QR url",
      "type": "n8n-nodes-base.code",
      "position": [
        944,
        352
      ],
      "parameters": {
        "jsCode": "const tab = String.fromCharCode(9);\nconst input = $input.first().json\nlet qr = `${input['passport_number']}${tab}${tab}${input['issue_date']}${tab}${input['expiry_date']}${tab}${tab}${tab}${input['issuing_place']}${tab}${input['fullname_telex']}${tab}${input['birth_date']}${tab}${tab}${input['sex']}${tab}${input['cmnd']}${tab}${input['birth_place']}${tab}${tab}${tab}${tab}${input['domestic_address']}${tab}${input['overseas_address']}`\nlet url = `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${encodeURIComponent(qr)}`\nreturn { url, name: input['fullname'] }"
      },
      "typeVersion": 2
    },
    {
      "id": "8f5268aa-e47d-41dc-882f-8b463db889ad",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1408,
        -448
      ],
      "parameters": {
        "width": 784,
        "height": 640,
        "content": "## Overview\nThis workflow extracts structured passport data using OpenAI OCR and generates QR codes from the extracted information.\n\n### How it works\n1. User submits passport images via form\n2. Each image is validated and resized\n3. OpenAI extracts passport fields\n4. Data is standardized and validated\n5. QR codes are generated\n6. Results delivered via form and email\n\n### Setup\n- Add OpenAI API credentials to OCR node\n- Connect Gmail for email delivery\n- Replace YOUR_EMAIL with your address\n- Test with clear passport images\n\n### Customization\n- Modify OCR prompt for other documents\n- Adjust QR format for your system"
      },
      "typeVersion": 1
    },
    {
      "id": "1dc541d7-61ff-4d10-8193-88debac5970c",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1408,
        672
      ],
      "parameters": {
        "color": 3,
        "width": 432,
        "height": 96,
        "content": "\u26a0\ufe0f Privacy: Personal identity data. Limit access and configure email carefully."
      },
      "typeVersion": 1
    },
    {
      "id": "53dbf4e2-d28f-457d-a70e-876c5fe7bb3b",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        288,
        240
      ],
      "parameters": {
        "color": 7,
        "width": 608,
        "height": 368,
        "content": "## Data extraction and QR generation\nSend images to OpenAI OCR, standardize extracted fields, validate results, create QR codes"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "9d7feeb2-4ebd-49b7-9af3-312f32bd9314",
  "connections": {
    "Grab image": {
      "main": [
        [
          {
            "node": "Form",
            "type": "main",
            "index": 0
          },
          {
            "node": "Send a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If is image": {
      "main": [
        [
          {
            "node": "Resize Image",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Loop Over images",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OCR - openAI": {
      "main": [
        [
          {
            "node": "Data standardization",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare form": {
      "main": [
        [
          {
            "node": "Loop Over images",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resize Image": {
      "main": [
        [
          {
            "node": "Update imageBase64",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If has result": {
      "main": [
        [
          {
            "node": "Prepare QR url",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Loop Over images",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare QR url": {
      "main": [
        [
          {
            "node": "Loop Over images",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over images": {
      "main": [
        [
          {
            "node": "Grab image",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "If is image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "On form submission": {
      "main": [
        [
          {
            "node": "Prepare form",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update imageBase64": {
      "main": [
        [
          {
            "node": "OCR - openAI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Data standardization": {
      "main": [
        [
          {
            "node": "If has result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow processes passport images submitted through a form, extracts structured data using OpenAI OCR, and generates QR codes with the extracted information. Results are displayed on the form completion page and sent via email.

Source: https://n8n.io/workflows/12868/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

Note: This template only works for self-hosted n8n.

Google Sheets Tool, Form Trigger, Form +3
AI & RAG

This n8n template retrieves verbal brand identity markers from any web site.

HTTP Request, Google Gemini, Form Trigger +1
AI & RAG

This workflow is for eCommerce researchers, affiliate marketers, and anyone who needs to compare product listings across sites like Amazon. It’s perfect for quickly identifying top product picks based

Form Trigger, HTTP Request, OpenAI +2
AI & RAG

This workflow is ideal for content creators, video marketers, and research professionals who need to extract actionable insights, detailed transcripts, or metadata from YouTube videos efficiently. It

HTTP Request, Google Drive, Gmail +2
AI & RAG

Legal, Procurement, and Compliance teams at mid-size companies. ESN and agencies selling AI-powered contract review as a service.

Form Trigger, HTTP Request, Form +4