This workflow corresponds to n8n.io template #12013 — we link there as the canonical source.
This workflow follows the Form Trigger → 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 →
{
"meta": {
"templateCredsSetupCompleted": true
},
"nodes": [
{
"id": "303a50d1-5b14-42e0-9fef-8f50b9cb04df",
"name": "Split Items to Create",
"type": "n8n-nodes-base.splitOut",
"position": [
4544,
2032
],
"parameters": {
"options": {},
"fieldToSplitOut": "itemsToCreate"
},
"typeVersion": 1
},
{
"id": "d4e09bce-aee2-4a4f-bbcf-4fe7d8c96f11",
"name": "Create Items",
"type": "n8n-nodes-base.httpRequest",
"position": [
4768,
2032
],
"parameters": {
"url": "https://sandbox-quickbooks.api.intuit.com/v3/company/{companyId}/item?minorversion=75",
"method": "POST",
"options": {},
"jsonBody": "={\n \"Name\": \"{{ $json.name }}\",\n \"Description\": \"{{ $json.description }}\",\n \"Type\": \"{{ $json.type }}\",\n \"IncomeAccountRef\": {\n \"value\": \"{{ $json.incomeAccountRef }}\"\n },\n \"ExpenseAccountRef\": {\n \"value\": \"{{ $json.expenseAccountRef }}\"\n }\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"nodeCredentialType": "quickBooksOAuth2Api"
},
"typeVersion": 4.3
},
{
"id": "2805b745-5725-460f-be2f-920bf4d444f4",
"name": "Merge Item Creation Paths",
"type": "n8n-nodes-base.merge",
"position": [
4992,
2104
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineAll"
},
"typeVersion": 3,
"alwaysOutputData": true
},
{
"id": "b7827159-538e-464a-870f-fcc0af7afd67",
"name": "Collect All Item Mappings",
"type": "n8n-nodes-base.code",
"position": [
5216,
2104
],
"parameters": {
"jsCode": "// Collect all item mappings from both existing and newly created items\nconst checkResult = $('Check Which Items to Create').first().json;\nconst itemMapping = { ...checkResult.existingItemMapping };\n\n// Add newly created items to mapping\ntry {\n const createdItems = $('Create Items').all();\n createdItems.forEach(node => {\n const item = node.json;\n // QuickBooks returns the item in an 'Item' wrapper\n const itemData = item.Item || item;\n if (itemData.Name && itemData.Id) {\n itemMapping[itemData.Name] = itemData.Id;\n }\n });\n} catch (error) {\n // No items were created, that's fine\n}\n\n// Get the extracted data\nconst extractedData = $('Prepare Items to Check').first().json.extractedData;\n\nreturn [{\n json: {\n extractedData: extractedData,\n allItemMapping: itemMapping\n }\n}];"
},
"typeVersion": 2
},
{
"id": "965884c8-9a44-4e8a-9603-501d43d34b2f",
"name": "Find Vendor",
"type": "n8n-nodes-base.quickbooks",
"position": [
5440,
2104
],
"parameters": {
"filters": {
"query": "=WHERE DisplayName = '{{ $json.extractedData.vendor.name }}'"
},
"resource": "vendor",
"operation": "getAll"
},
"typeVersion": 1
},
{
"id": "a6548ed0-e56f-4c74-b9ea-d391122c855a",
"name": "Vendor Exists?",
"type": "n8n-nodes-base.if",
"position": [
5664,
2104
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "1",
"operator": {
"type": "string",
"operation": "empty"
},
"leftValue": "={{ $json.Id }}"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "7eb10846-d37c-4b5b-a11e-d4392e2b77c2",
"name": "Create Vendor",
"type": "n8n-nodes-base.quickbooks",
"position": [
5904,
2032
],
"parameters": {
"resource": "vendor",
"operation": "create",
"displayName": "={{ $('Collect All Item Mappings').first().json.extractedData.vendor.name }}",
"additionalFields": {
"CompanyName": "={{ $('Collect All Item Mappings').first().json.extractedData.vendor.name }}",
"PrimaryEmailAddr": "={{ $('Collect All Item Mappings').first().json.extractedData.vendor.email }}"
}
},
"typeVersion": 1
},
{
"id": "e96e6a97-14b1-4269-a7cc-d3130a1a8a63",
"name": "Create Bill ",
"type": "n8n-nodes-base.httpRequest",
"position": [
6368,
2144
],
"parameters": {
"url": "https://sandbox-quickbooks.api.intuit.com/v3/company/{companyId}/bill?minorversion=75",
"method": "POST",
"options": {},
"jsonBody": "={{ JSON.stringify($json.billPayload) }}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"nodeCredentialType": "quickBooksOAuth2Api"
},
"typeVersion": 4.2
},
{
"id": "49f364de-8174-4e01-bf7b-d2be3286818a",
"name": "Need to Create Items?",
"type": "n8n-nodes-base.if",
"position": [
4320,
2096
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "1",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.needsCreation }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "6c671cc9-6009-484a-ae8a-67f2a7e90324",
"name": "Get All QB Items",
"type": "n8n-nodes-base.quickbooks",
"position": [
3872,
2096
],
"parameters": {
"filters": {},
"resource": "item",
"operation": "getAll",
"returnAll": true
},
"typeVersion": 1
},
{
"id": "84340514-dab8-4a14-b9ba-edc76ab396fd",
"name": "Prepare Items to Check",
"type": "n8n-nodes-base.code",
"position": [
3680,
2096
],
"parameters": {
"jsCode": "const extractedData = $input.first().json.output;\nconst lineItems = extractedData.line_items || [];\n\nconst itemsToCheck = lineItems\n .filter(item => item.item_name && item.item_name.trim() !== '')\n .map(item => ({\n name: item.item_name,\n description: item.description || item.item_name,\n unitPrice: parseFloat(item.unit_price) || parseFloat(item.amount),\n type: 'Service'\n }));\n\nreturn [{\n json: {\n extractedData: extractedData,\n itemsToCheck: itemsToCheck\n }\n}];"
},
"typeVersion": 2
},
{
"id": "ed2369bb-2c7f-461f-aeb5-eed9391e4402",
"name": "Extract Invoice Data",
"type": "@n8n/n8n-nodes-langchain.informationExtractor",
"position": [
3296,
2104
],
"parameters": {
"text": "={{ $json.cleaned_text }}",
"options": {
"systemPromptTemplate": "Extract invoice data accurately. Extract vendor info, line items with item names, quantities, unit prices, and amounts. Return numbers as numbers, not strings."
},
"schemaType": "fromJson",
"jsonSchemaExample": "{\n \"invoice_number\": \"\",\n \"invoice_date\": \"\",\n \"due_date\": \"\",\n \"vendor\": {\n \"name\": \"\",\n \"address\": \"\",\n \"email\": \"\",\n \"phone\": \"\"\n },\n \"line_items\": [\n {\n \"item_name\": \"\",\n \"description\": \"\",\n \"quantity\": 0,\n \"unit_price\": 0,\n \"amount\": 0\n }\n ],\n \"subtotal\": 0,\n \"tax\": 0,\n \"total\": 0\n}"
},
"typeVersion": 1.2
},
{
"id": "ecf48035-31bc-43b8-8a2a-f4de560f134b",
"name": "Clean Text",
"type": "n8n-nodes-base.code",
"position": [
3056,
2096
],
"parameters": {
"jsCode": "const text = $json[\"text\"] || '';\nlet data = '';\n\nif (Object.keys($binary || {}).length) {\n data = $binary.data ? Buffer.from($binary.data.data).toString('utf8') : '';\n}\n\nconst cleaned = (text || data || '').replace(/\\r\\n/g,'\\n').replace(/\\n+/g,'\\n').trim();\n\nreturn [{ \n json: { \n cleaned_text: cleaned \n } \n}];"
},
"typeVersion": 2
},
{
"id": "3ee694dd-3899-4355-8687-d8bd83ad8bf8",
"name": "Loop Over Invoices",
"type": "n8n-nodes-base.splitInBatches",
"position": [
2624,
2228
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "11125910-971f-4829-a010-d98ce0de67d8",
"name": "Convert to Separate Items",
"type": "n8n-nodes-base.code",
"position": [
2400,
2228
],
"parameters": {
"jsCode": "const item = $input.first();\nconst binaries = item.binary || {};\nconst output = [];\n\nfor (const key of Object.keys(binaries)) {\n output.push({\n json: { \n file: key,\n filename: key,\n mimeType: binaries[key].mimeType,\n fileSize: binaries[key].fileSize\n },\n binary: {\n data: binaries[key]\n }\n });\n}\n\nreturn output;"
},
"typeVersion": 2
},
{
"id": "c123a41e-5799-4f30-876b-addff0693431",
"name": "Extract from PDF",
"type": "n8n-nodes-base.extractFromFile",
"position": [
2848,
2104
],
"parameters": {
"options": {},
"operation": "pdf"
},
"typeVersion": 1.1
},
{
"id": "5020b30e-b4ba-415d-b506-63db5368419c",
"name": "Check Which Items to Create",
"type": "n8n-nodes-base.code",
"position": [
4096,
2096
],
"parameters": {
"jsCode": "const itemsToCheck = $('Prepare Items to Check').first().json.itemsToCheck;\nconst existingItems = $input.all();\n\nconst existingItemMap = {};\nexistingItems.forEach(itemNode => {\n const item = itemNode.json;\n if (item.Name) {\n existingItemMap[item.Name.toLowerCase()] = {\n id: item.Id,\n name: item.Name\n };\n }\n});\n\nconst itemsToCreate = [];\nconst itemMapping = {};\n\nitemsToCheck.forEach(item => {\n const itemNameLower = item.name.toLowerCase();\n \n if (existingItemMap[itemNameLower]) {\n itemMapping[item.name] = existingItemMap[itemNameLower].id;\n } else {\n itemsToCreate.push({\n name: item.name,\n description: item.description,\n type: item.type,\n incomeAccountRef: \"79\",\n expenseAccountRef: \"80\"\n });\n }\n});\n\nreturn [{\n json: {\n itemsToCreate: itemsToCreate,\n existingItemMapping: itemMapping,\n needsCreation: itemsToCreate.length > 0\n }\n}];"
},
"typeVersion": 2
},
{
"id": "30ab1e9d-703f-4c78-8495-3399f8e793dd",
"name": "Build Bill Payload",
"type": "n8n-nodes-base.code",
"position": [
6144,
2144
],
"parameters": {
"jsCode": "const extractedData = $('Collect All Item Mappings').first().json.extractedData;\nconst itemMapping = $('Collect All Item Mappings').first().json.allItemMapping;\n\n// Get vendor info\nlet vendorId, vendorName;\n\ntry {\n const vendorNode = $('Create Vendor').first();\n vendorId = vendorNode.json.Id;\n vendorName = vendorNode.json.DisplayName;\n} catch {\n const vendorNode = $('Find Vendor').first();\n vendorId = vendorNode.json.Id;\n vendorName = vendorNode.json.DisplayName;\n}\n\nif (!vendorId) {\n throw new Error(`Vendor not found: ${extractedData.vendor.name}`);\n}\n\n// Build line items\nconst lineItems = [];\n\nextractedData.line_items.forEach((item, index) => {\n // Try exact match first, then case-insensitive match\n let itemId = itemMapping[item.item_name];\n \n if (!itemId) {\n // Try case-insensitive lookup\n const itemNameLower = item.item_name.toLowerCase();\n for (const [key, value] of Object.entries(itemMapping)) {\n if (key.toLowerCase() === itemNameLower) {\n itemId = value;\n break;\n }\n }\n }\n \n if (!itemId) {\n throw new Error(`Item not found: ${item.item_name}`);\n }\n \n const quantity = parseFloat(item.quantity) || 1;\n const unitPrice = parseFloat(item.unit_price) || (parseFloat(item.amount) / quantity);\n \n lineItems.push({\n LineNum: index + 1,\n Description: item.description || item.item_name,\n Amount: parseFloat(item.amount),\n DetailType: \"ItemBasedExpenseLineDetail\",\n ItemBasedExpenseLineDetail: {\n ItemRef: {\n value: itemId,\n name: item.item_name\n },\n Qty: quantity,\n UnitPrice: unitPrice,\n BillableStatus: \"NotBillable\",\n TaxCodeRef: {\n value: \"NON\"\n }\n }\n });\n});\n\n// Add tax if exists\nif (extractedData.tax && parseFloat(extractedData.tax) > 0) {\n lineItems.push({\n LineNum: lineItems.length + 1,\n Description: 'Tax',\n Amount: parseFloat(extractedData.tax),\n DetailType: 'AccountBasedExpenseLineDetail',\n AccountBasedExpenseLineDetail: {\n AccountRef: {\n value: '1'\n },\n BillableStatus: 'NotBillable',\n TaxCodeRef: {\n value: 'NON'\n }\n }\n });\n}\n\n// Parse dates\nlet billDate = extractedData.invoice_date;\nlet dueDate = extractedData.due_date;\n\nif (billDate && !billDate.match(/^\\d{4}-\\d{2}-\\d{2}$/)) {\n billDate = new Date(billDate).toISOString().split('T')[0];\n}\n\nif (dueDate && !dueDate.match(/^\\d{4}-\\d{2}-\\d{2}$/)) {\n dueDate = new Date(dueDate).toISOString().split('T')[0];\n}\n\n// Build bill payload\nconst billPayload = {\n Line: lineItems,\n VendorRef: {\n value: vendorId,\n name: vendorName\n },\n DocNumber: extractedData.invoice_number,\n TxnDate: billDate,\n DueDate: dueDate || billDate\n};\n\nif (extractedData.vendor?.address) {\n const addressLines = extractedData.vendor.address.split('\\n');\n billPayload.VendorAddr = {\n Line1: addressLines[0] || '',\n Line2: addressLines[1] || '',\n Line3: addressLines[2] || ''\n };\n}\n\nreturn {\n json: {\n billPayload: billPayload,\n summary: {\n vendor: vendorName,\n billNumber: extractedData.invoice_number,\n total: extractedData.total,\n lineItems: lineItems.length\n }\n }\n};"
},
"typeVersion": 2
},
{
"id": "6058dc16-7403-48c5-ad87-c27f9bde5138",
"name": "OpenRouter Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
"position": [
3296,
2400
],
"parameters": {
"model": "openai/gpt-oss-20b:free",
"options": {}
},
"typeVersion": 1
},
{
"id": "dd2fbbf5-ca57-4821-be26-3872991e6f0c",
"name": "On bill submission",
"type": "n8n-nodes-base.formTrigger",
"position": [
2176,
2228
],
"parameters": {
"options": {},
"formTitle": "Invoice Parser + QuickBooks Bill Creator",
"formFields": {
"values": [
{
"fieldType": "file",
"fieldLabel": "Invoice File",
"requiredField": true,
"acceptFileTypes": ".pdf"
}
]
}
},
"typeVersion": 2.3
},
{
"id": "77a10310-6a8c-4eba-a058-15e0af6d8b32",
"name": "Workflow Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
1696,
2096
],
"parameters": {
"width": 420,
"height": 400,
"content": "## How it works\n1. **Upload**: Submit PDF invoices via web form\n2. **Extract**: AI reads invoice data (vendor, items, amounts, dates)\n3. **Reconcile**: Checks if items/vendors exist in QuickBooks\n4. **Create**: Auto-creates missing items and vendors\n5. **Bill**: Generates complete bill in QuickBooks with all line items\n\n## Setup steps\n1. Connect QuickBooks OAuth2 credentials\n2. Add OpenRouter API key for AI extraction\n3. Configure income/expense account refs (currently 79/80)\n4. Test with sample invoice PDF\n5. Access form URL to submit invoices"
},
"typeVersion": 1
},
{
"id": "d3ec43fd-2232-4e5c-a537-39573a03d725",
"name": "PDF Processing",
"type": "n8n-nodes-base.stickyNote",
"position": [
2128,
2096
],
"parameters": {
"color": 7,
"width": 652,
"height": 380,
"content": "## PDF Processing\nConverts uploaded PDFs to separate items and loops through each invoice for extraction."
},
"typeVersion": 1
},
{
"id": "adde0363-ce66-413b-8448-ebcbadaeb596",
"name": "AI Extraction",
"type": "n8n-nodes-base.stickyNote",
"position": [
3216,
1952
],
"parameters": {
"color": 7,
"width": 364,
"height": 604,
"content": "## AI Extraction\n\nExtracts structured data from invoice text using OpenRouter LLM (vendor, line items, dates, amounts)."
},
"typeVersion": 1
},
{
"id": "82616fc6-1e70-4c65-ac8d-b8b6f0beff8d",
"name": "Item Management",
"type": "n8n-nodes-base.stickyNote",
"position": [
3600,
1952
],
"parameters": {
"color": 7,
"width": 1572,
"height": 396,
"content": "## Item Management\n\nChecks existing QuickBooks items against invoice line items. Creates missing items automatically with default accounts."
},
"typeVersion": 1
},
{
"id": "57243061-86f8-472b-a02a-df79f67370b3",
"name": "Vendor Management",
"type": "n8n-nodes-base.stickyNote",
"position": [
5184,
1952
],
"parameters": {
"color": 7,
"width": 884,
"height": 396,
"content": "## Vendor Management\n\nSearches for vendor by name. Creates new vendor record if not found, using extracted contact info."
},
"typeVersion": 1
},
{
"id": "9ffda906-d1b5-4fc5-98ce-c2311d849e7f",
"name": "Bill Creation",
"type": "n8n-nodes-base.stickyNote",
"position": [
6080,
1952
],
"parameters": {
"color": 7,
"width": 440,
"height": 396,
"content": "## Bill Creation\n\nBuilds complete bill payload with all line items, vendor ref, dates, and tax. Posts to QuickBooks API."
},
"typeVersion": 1
}
],
"connections": {
"Clean Text": {
"main": [
[
{
"node": "Extract Invoice Data",
"type": "main",
"index": 0
}
]
]
},
"Find Vendor": {
"main": [
[
{
"node": "Vendor Exists?",
"type": "main",
"index": 0
}
]
]
},
"Create Bill ": {
"main": [
[
{
"node": "Loop Over Invoices",
"type": "main",
"index": 0
}
]
]
},
"Create Items": {
"main": [
[
{
"node": "Merge Item Creation Paths",
"type": "main",
"index": 0
}
]
]
},
"Create Vendor": {
"main": [
[
{
"node": "Build Bill Payload",
"type": "main",
"index": 0
}
]
]
},
"Vendor Exists?": {
"main": [
[
{
"node": "Create Vendor",
"type": "main",
"index": 0
}
],
[
{
"node": "Build Bill Payload",
"type": "main",
"index": 0
}
]
]
},
"Extract from PDF": {
"main": [
[
{
"node": "Clean Text",
"type": "main",
"index": 0
}
]
]
},
"Get All QB Items": {
"main": [
[
{
"node": "Check Which Items to Create",
"type": "main",
"index": 0
}
]
]
},
"Build Bill Payload": {
"main": [
[
{
"node": "Create Bill ",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Invoices": {
"main": [
[],
[
{
"node": "Extract from PDF",
"type": "main",
"index": 0
}
]
]
},
"On bill submission": {
"main": [
[
{
"node": "Convert to Separate Items",
"type": "main",
"index": 0
}
]
]
},
"Extract Invoice Data": {
"main": [
[
{
"node": "Prepare Items to Check",
"type": "main",
"index": 0
}
]
]
},
"Need to Create Items?": {
"main": [
[
{
"node": "Split Items to Create",
"type": "main",
"index": 0
}
],
[
{
"node": "Merge Item Creation Paths",
"type": "main",
"index": 1
}
]
]
},
"OpenRouter Chat Model": {
"ai_languageModel": [
[
{
"node": "Extract Invoice Data",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Split Items to Create": {
"main": [
[
{
"node": "Create Items",
"type": "main",
"index": 0
}
]
]
},
"Prepare Items to Check": {
"main": [
[
{
"node": "Get All QB Items",
"type": "main",
"index": 0
}
]
]
},
"Collect All Item Mappings": {
"main": [
[
{
"node": "Find Vendor",
"type": "main",
"index": 0
}
]
]
},
"Convert to Separate Items": {
"main": [
[
{
"node": "Loop Over Invoices",
"type": "main",
"index": 0
}
]
]
},
"Merge Item Creation Paths": {
"main": [
[
{
"node": "Collect All Item Mappings",
"type": "main",
"index": 0
}
]
]
},
"Check Which Items to Create": {
"main": [
[
{
"node": "Need to Create Items?",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Accounting teams spend hours manually entering purchase bills into accounting systems—copying vendor details, creating items, checking duplicates, and reconciling totals. This workflow removes that manual effort entirely.
Source: https://n8n.io/workflows/12013/ — 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.
Spot Workplace Discrimination Patterns with AI. Uses manualTrigger, lmChatOpenAi, httpRequest, html. Event-driven trigger; 38 nodes.
Spot Workplace Discrimination Patterns with AI. Uses manualTrigger, lmChatOpenAi, httpRequest, html. Event-driven trigger; 38 nodes.
This template can be used to find the content gaps in your competitors' discourse: identifying the topics they are not yet connecting and giving you an opportunity to fill in this gap with your conten
Animal advocates & campaigners who want a weekly briefing on animal-related bills with clear, actionable steps—no manual research needed.
Stopanderror Extractfromfile. Uses manualTrigger, stickyNote, extractFromFile, informationExtractor. Event-driven trigger; 24 nodes.