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 →
{
"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.
gmailOAuth2
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 →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Note: This template only works for self-hosted n8n.
This n8n template retrieves verbal brand identity markers from any web site.
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
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
Legal, Procurement, and Compliance teams at mid-size companies. ESN and agencies selling AI-powered contract review as a service.