AutomationFlowsSlack & Telegram › 04 · Invoice OCR → Google Sheets

04 · Invoice OCR → Google Sheets

04 · Invoice OCR → Google Sheets. Uses telegramTrigger, telegram, httpRequest, googleSheets. Event-driven trigger; 10 nodes.

Event trigger★★★★☆ complexity10 nodesTelegram TriggerTelegramHTTP RequestGoogle Sheets
Slack & Telegram Trigger: Event Nodes: 10 Complexity: ★★★★☆ Added:

This workflow follows the Google Sheets → 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
{
  "name": "04 \u00b7 Invoice OCR \u2192 Google Sheets",
  "nodes": [
    {
      "parameters": {
        "content": "## 04 \u00b7 Invoice OCR \u2192 spreadsheet\n**Telegram photo/PDF \u2192 download \u2192 GPT vision \u2192 validate \u2192 Google Sheets**\n\nSend a photo of a receipt to the bot and the line lands in the sheet.\n\nLow-confidence or zero-total extractions never reach the sheet \u2014 the\nuser is asked for a better photo instead. Silent bad data is worse\nthan no data.",
        "height": 300,
        "width": 560,
        "color": 7
      },
      "id": "00000004-0000-4000-8000-000000000000",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -300,
        -240
      ]
    },
    {
      "parameters": {
        "updates": [
          "message"
        ],
        "additionalFields": {
          "download": false
        }
      },
      "id": "00000004-0000-4000-8000-000000000001",
      "name": "Invoice Received",
      "type": "n8n-nodes-base.telegramTrigger",
      "typeVersion": 1.1,
      "position": [
        -280,
        160
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "file",
        "fileId": "={{ $json.message.document?.file_id || $json.message.photo?.[$json.message.photo.length - 1]?.file_id }}",
        "download": true
      },
      "id": "00000004-0000-4000-8000-000000000002",
      "name": "Download File",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        -60,
        160
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000
    },
    {
      "parameters": {
        "jsCode": "// The vision endpoint wants a data URL, so turn the downloaded binary into\n// base64 here rather than relying on version-specific $binary expressions.\nconst items = $input.all();\nconst out = [];\n\nfor (let index = 0; index < items.length; index++) {\n  const meta = items[index].binary?.data;\n  if (!meta) {\n    throw new Error('No binary property \"data\" - check the Telegram file node.');\n  }\n\n  const buffer = await this.helpers.getBinaryDataBuffer(index, 'data');\n  out.push({\n    json: {\n      chat_id: $('Invoice Received').first().json.message?.chat?.id ?? '',\n      file_name: meta.fileName ?? 'invoice',\n      mime_type: meta.mimeType ?? 'image/jpeg',\n      base64: buffer.toString('base64'),\n    },\n  });\n}\n\nreturn out;"
      },
      "id": "00000004-0000-4000-8000-000000000003",
      "name": "Image to Base64",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        160,
        160
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"model\": \"gpt-4o-mini\",\n  \"temperature\": 0,\n  \"max_tokens\": 800,\n  \"response_format\": { \"type\": \"json_object\" },\n  \"messages\": [\n    {\n      \"role\": \"system\",\n      \"content\": \"You extract structured data from invoices and receipts. Reply with JSON only, using exactly these keys: vendor, invoice_number, issue_date (YYYY-MM-DD or empty), due_date (YYYY-MM-DD or empty), currency (ISO 4217), subtotal (number), tax (number), total (number), category (software|hardware|travel|services|utilities|other), confidence (0-1). Use 0 for any amount you cannot read and lower the confidence accordingly. Never guess a total from partial digits.\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": [\n        { \"type\": \"text\", \"text\": \"Extract the invoice data.\" },\n        { \"type\": \"image_url\", \"image_url\": { \"url\": \"data:{{ $json.mime_type }};base64,{{ $json.base64 }}\" } }\n      ]\n    }\n  ]\n}",
        "options": {
          "timeout": 60000
        }
      },
      "id": "00000004-0000-4000-8000-000000000004",
      "name": "Extract Invoice Data",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        380,
        160
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000
    },
    {
      "parameters": {
        "jsCode": "const source = $('Image to Base64').first().json;\nconst raw = $json.choices?.[0]?.message?.content ?? '{}';\n\nlet invoice;\ntry {\n  invoice = JSON.parse(raw);\n} catch (error) {\n  invoice = { confidence: 0, total: 0 };\n}\n\nconst number = (value) => {\n  const parsed = Number(String(value ?? '').replace(',', '.').replace(/[^0-9.\\-]/g, ''));\n  return Number.isFinite(parsed) ? parsed : 0;\n};\n\nreturn [\n  {\n    json: {\n      chat_id: source.chat_id,\n      file_name: source.file_name,\n      vendor: invoice.vendor ?? '',\n      invoice_number: invoice.invoice_number ?? '',\n      issue_date: invoice.issue_date ?? '',\n      due_date: invoice.due_date ?? '',\n      currency: (invoice.currency ?? '').toUpperCase(),\n      subtotal: number(invoice.subtotal),\n      tax: number(invoice.tax),\n      total: number(invoice.total),\n      category: invoice.category ?? 'other',\n      confidence: Number(invoice.confidence ?? 0),\n      extracted_at: new Date().toISOString(),\n    },\n  },\n];"
      },
      "id": "00000004-0000-4000-8000-000000000005",
      "name": "Normalize Invoice",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        600,
        160
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "c1",
              "leftValue": "={{ $json.total }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            },
            {
              "id": "c2",
              "leftValue": "={{ $json.confidence }}",
              "rightValue": 0.6,
              "operator": {
                "type": "number",
                "operation": "gte"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "00000004-0000-4000-8000-000000000006",
      "name": "Extraction Trustworthy?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        820,
        160
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "REPLACE_WITH_GOOGLE_SHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Expenses",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Extracted At": "={{ $json.extracted_at }}",
            "Vendor": "={{ $json.vendor }}",
            "Invoice Number": "={{ $json.invoice_number }}",
            "Issue Date": "={{ $json.issue_date }}",
            "Due Date": "={{ $json.due_date }}",
            "Currency": "={{ $json.currency }}",
            "Subtotal": "={{ $json.subtotal }}",
            "Tax": "={{ $json.tax }}",
            "Total": "={{ $json.total }}",
            "Category": "={{ $json.category }}",
            "Confidence": "={{ $json.confidence }}"
          },
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "id": "00000004-0000-4000-8000-000000000007",
      "name": "Append to Expenses",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1060,
        60
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000
    },
    {
      "parameters": {
        "chatId": "={{ $json.chat_id }}",
        "text": "=\u2705 <b>{{ $json.vendor }}</b>\n{{ $json.total }} {{ $json.currency }} \u00b7 {{ $json.category }}\nInvoice {{ $json.invoice_number || '\u2014' }} \u00b7 due {{ $json.due_date || '\u2014' }}",
        "additionalFields": {
          "parse_mode": "HTML"
        }
      },
      "id": "00000004-0000-4000-8000-000000000008",
      "name": "Confirm to User",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1280,
        60
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "chatId": "={{ $json.chat_id }}",
        "text": "=\u26a0\ufe0f I could not read that invoice reliably (confidence {{ $json.confidence }}).\nSend a sharper, well-lit photo with the total visible.",
        "additionalFields": {
          "parse_mode": "HTML"
        }
      },
      "id": "00000004-0000-4000-8000-000000000009",
      "name": "Ask for a Better Photo",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1060,
        280
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    }
  ],
  "connections": {
    "Invoice Received": {
      "main": [
        [
          {
            "node": "Download File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download File": {
      "main": [
        [
          {
            "node": "Image to Base64",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Image to Base64": {
      "main": [
        [
          {
            "node": "Extract Invoice Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Invoice Data": {
      "main": [
        [
          {
            "node": "Normalize Invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Invoice": {
      "main": [
        [
          {
            "node": "Extraction Trustworthy?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extraction Trustworthy?": {
      "main": [
        [
          {
            "node": "Append to Expenses",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Ask for a Better Photo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append to Expenses": {
      "main": [
        [
          {
            "node": "Confirm to User",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "saveManualExecutions": true
  },
  "tags": []
}

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

04 · Invoice OCR → Google Sheets. Uses telegramTrigger, telegram, httpRequest, googleSheets. Event-driven trigger; 10 nodes.

Source: https://github.com/wanderfool95/n8n-ai-workflows/blob/main/workflows/04-invoice-ocr-to-sheet.json — original creator credit. Request a take-down →

More Slack & Telegram workflows → · Browse all categories →

Related workflows

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

Slack & Telegram

This workflow provides a complete solution for handling Telegram Stars payments, invoicing and refunds using n8n. It automates the process of sending invoices, managing pre-checkout approvals, recordi

HTTP Request, Execute Workflow Trigger, Google Sheets +2
Slack & Telegram

clients kept booking meetings during my prayer times. i'd either miss a prayer or scramble to reschedule. the problem wasn't the clients — it was that my calendar had no blocked windows for salah. i n

Telegram Trigger, HTTP Request, Google Calendar +3
Slack & Telegram

Generate 360° product videos from a single photo using Google Veo 3 and Telegram

Telegram, Telegram Trigger, HTTP Request +1
Slack & Telegram

02b — Article callback. Uses telegramTrigger, googleSheets, telegram, httpRequest. Event-driven trigger; 30 nodes.

Telegram Trigger, Google Sheets, Telegram +1
Slack & Telegram

Automates LinkedIn job searches across multiple countries and categories, filters results with AI, stores data in Google Sheets, and sends weekly Telegram notifications. Perfect for professionals seek

Telegram Trigger, Item Lists, HTTP Request +3