{
  "name": "ScanKit: Invoice scan to structured row",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "scankit-invoice-inbox",
        "responseMode": "onReceived",
        "responseData": "noData",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        0,
        0
      ],
      "id": "4fb15a5e-6368-4d41-9032-436b323d287e",
      "name": "ScanKit Invoice Webhook",
      "webhookId": "dca23f5c-ea76-4368-9cae-bc8b3d30f87a"
    },
    {
      "parameters": {
        "jsCode": "// Normalize the ScanKit webhook envelope.\n// ScanKit sends ONLY the cleaned scan:\n//   multipart: file (scan.pdf) + metadata JSON\n//     { scanner_id, scanner_name, page_count, timestamp }\n//   JSON path: { scanner_id, scanner_name, results: [...], timestamp }\n// No OCR text, no confidence score, no CDN url. Extraction happens later.\nconst item = { json: {}, binary: {} };\nlet meta = {};\nif (typeof $json.metadata === 'string') {\n  try { meta = JSON.parse($json.metadata); } catch (e) { meta = {}; }\n} else if ($json.metadata && typeof $json.metadata === 'object') {\n  meta = $json.metadata;\n} else {\n  meta = $json; // JSON path: whole body carries scanner fields\n}\n// Fictional example values are used when the envelope is empty.\nconst envelope = {\n  scanner_id: meta.scanner_id || 42,                    // fictional\n  scanner_name: meta.scanner_name || 'Invoice Inbox',   // fictional\n  page_count: meta.page_count || 1,\n  timestamp: meta.timestamp || new Date().toISOString(),\n};\nitem.json = envelope;\n// Keep the uploaded PDF for the extraction step (if present as binary).\nif ($binary && Object.keys($binary).length) {\n  item.binary.scan = $binary[Object.keys($binary)[0]];\n}\nreturn [item];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ],
      "id": "4c102e0d-7926-4082-8ff9-b20d55ac2222",
      "name": "Normalize ScanKit payload"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR-VISION-LLM-ENDPOINT.example/v1/chat/completions",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: \"your-vision-model\", messages: [{ role: \"user\", content: \"Extract supplier, invoice_number, date, total from this scan. Return JSON only.\" }] }) }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        0
      ],
      "id": "0f1afa4a-7512-4d28-b883-6986a7637ea3",
      "name": "Vision LLM: extract invoice JSON"
    },
    {
      "parameters": {
        "jsCode": "// Parse the LLM JSON response and validate required fields.\nlet out = { json: { ok: false, fields: {}, reason: 'no response' }, binary: {} };\nif ($binary && $binary.data) { out.binary.scan = $binary.data; }\ntry {\n  let text = '';\n  const body = $json;\n  if (body && typeof body === 'string') text = body;\n  else if (body && body.choices && body.choices[0]) text = body.choices[0].message && body.choices[0].message.content || JSON.stringify(body);\n  else if (body && body.message && body.message.content) text = body.message.content;\n  else text = JSON.stringify(body);\n  // The endpoint may already return parsed JSON; accept both shapes.\n  let parsed = text;\n  try { parsed = JSON.parse(text); } catch (e) { /* text was not JSON */ }\n  if (typeof parsed === 'string') { try { parsed = JSON.parse(parsed); } catch (e2) {} }\n  const f = parsed.fields || parsed.parsed || (parsed.supplier ? parsed : {});\n  const total = Number(f.total);\n  const date = new Date(f.date);\n  const ok = !!(f.supplier && f.invoice_number && total > 0 && !isNaN(date.getTime()));\n  out.json = { ok, fields: f, total, reason: ok ? 'valid' : 'missing or invalid fields' };\n} catch (e) {\n  out.json = { ok: false, fields: {}, reason: String(e && e.message || e) };\n}\nreturn [out];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        0
      ],
      "id": "2c071b81-e0b8-4584-93ff-7c8081fa0606",
      "name": "Validate extraction"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "1d40005d-15a7-44fa-b785-e98bf82b1994",
              "leftValue": "={{ $json.ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        880,
        0
      ],
      "id": "d7c9f309-3d96-40a9-b727-d749b1335b53",
      "name": "Valid extraction?"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR-SHEETS-OR-ERP-ENDPOINT.example/rows",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.fields) }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1100,
        -160
      ],
      "id": "cf926449-c06e-40b0-92e0-2768246010b2",
      "name": "Append row (Sheets/ERP endpoint)"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR-REVIEW-QUEUE-ENDPOINT.example/review",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ scanner: $json.scanner_name || null, reason: $json.reason || \"invalid\", fields: $json.fields || {} }) }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1100,
        160
      ],
      "id": "bf12ed11-8d9b-47c8-840d-362789f8dcc0",
      "name": "Send to human review"
    },
    {
      "parameters": {
        "content": "Workflow template: invoice scan to structured row.\n\n1. Create a hosted ScanKit scanner and set THIS workflow webhook URL as destination.\n2. The webhook receives the cleaned scan (file scan.pdf + metadata JSON). ScanKit does not send OCR text or a confidence score.\n3. Replace the Vision LLM endpoint (marked YOUR-VISION-LLM-ENDPOINT) with your own vision-capable LLM API and add authentication.\n4. Replace the Sheets/ERP endpoint and the review endpoint with your targets.\n5. Failed extractions land in the review branch, never in your sheet.\n\nAll example values are fictional.",
        "height": 360,
        "width": 300
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        440,
        340
      ],
      "id": "73dc01b2-eb32-43b3-a42a-c0c5b1c6f65e",
      "name": "How to use"
    }
  ],
  "pinData": {},
  "connections": {
    "ScanKit Invoice Webhook": {
      "main": [
        [
          {
            "node": "Normalize ScanKit payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize ScanKit payload": {
      "main": [
        [
          {
            "node": "Vision LLM: extract invoice JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Vision LLM: extract invoice JSON": {
      "main": [
        [
          {
            "node": "Validate extraction",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate extraction": {
      "main": [
        [
          {
            "node": "Valid extraction?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Valid extraction?": {
      "main": [
        [
          {
            "node": "Append row (Sheets/ERP endpoint)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send to human review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "bb6065ca-a0d4-4c60-907a-adc522b6b384",
  "meta": {},
  "id": "645c576d-f5ec-42a9-b373-07feaf310074",
  "tags": []
}