This workflow corresponds to n8n.io template #14252 — we link there as the canonical source.
This workflow follows the HTTP Request → Telegram 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": "lclnPF994zvSrslv",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Indonesian Stock (IDX) Invoice Reader",
"tags": [],
"nodes": [
{
"id": "fbb15b1c-2856-480e-b324-1bda0624c0a3",
"name": "Sticky Note - Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-400,
352
],
"parameters": {
"width": 460,
"height": 724,
"content": "## \ud83c\uddee\ud83c\udde9 Indonesian Stock (IDX) Invoice Reader\n\nAutomatically extract stock transactions from Indonesian broker trade confirmations sent via Telegram.\n\n### How it works\n1. Send a broker PDF or image to your Telegram bot\n2. The file is downloaded and encoded as base64\n3. OpenRouter (Gemini) extracts all transactions as structured JSON\n4. A single batch confirmation is sent with \u2705 / \u274c buttons\n5. On confirm, the structured data is output \u2014 connect to any destination node\n\n### Setup\n1. Create a bot via BotFather \u2192 add **Telegram account** credential\n2. Get an OpenRouter API key \u2192 add **Header Auth** credential named **OpenRouter API** with value `Authorization: Bearer sk-or-...`\n3. Expose n8n via HTTPS (Cloudflare Tunnel, ngrok, or a public server)\n4. Activate the workflow \u2014 Telegram webhook registers automatically\n\n### Customization\nSwap the **Send Confirmation** output to any destination (HTTP Request, Google Sheets, Airtable, etc.). Change the model in **Build Request** \u2014 default is `google/gemini-2.5-flash-lite`."
},
"typeVersion": 1
},
{
"id": "2ce04cd4-03ed-4d90-97f4-c40460b2e9d1",
"name": "Sticky Note - Message Intake",
"type": "n8n-nodes-base.stickyNote",
"position": [
112,
400
],
"parameters": {
"color": 7,
"width": 680,
"height": 320,
"content": "## \ud83d\udce8 Message Intake\nReceives all Telegram updates (messages and button callbacks), parses the incoming data, and routes file uploads to the processing branch."
},
"typeVersion": 1
},
{
"id": "d200c299-1aca-4de9-b1ac-358629c16d36",
"name": "Sticky Note - File Processing",
"type": "n8n-nodes-base.stickyNote",
"position": [
768,
768
],
"parameters": {
"color": 7,
"width": 660,
"height": 304,
"content": "## \ud83d\udcc1 File Processing\nDownloads the file from Telegram servers, reads the binary data, and encodes it as base64 ready for the AI request."
},
"typeVersion": 1
},
{
"id": "d8110936-d046-41fc-bb9c-9e0c7e13b594",
"name": "Sticky Note - AI Extraction",
"type": "n8n-nodes-base.stickyNote",
"position": [
1344,
416
],
"parameters": {
"color": 7,
"width": 660,
"height": 304,
"content": "## \ud83e\udd16 AI Extraction\nBuilds the OpenRouter request (PDF or image format), calls Gemini, and parses the JSON array of transactions from the response."
},
"typeVersion": 1
},
{
"id": "064977ba-9df1-417d-b95d-b6b1a6bd81f4",
"name": "Sticky Note - Confirmation",
"type": "n8n-nodes-base.stickyNote",
"position": [
2048,
768
],
"parameters": {
"color": 7,
"width": 660,
"height": 476,
"content": "## \u2705 Confirmation\nGroups all extracted transactions into a batch, stores them temporarily in workflow static data, and sends a confirmation message with Save All / Cancel buttons."
},
"typeVersion": 1
},
{
"id": "e5b0a373-003c-4cb8-90b4-d58eaf3b66f0",
"name": "Route Type1",
"type": "n8n-nodes-base.switch",
"position": [
592,
544
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "callback",
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "1a3a40ba-e140-46bd-b339-f26348495875",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.is_callback }}"
}
]
},
"renameOutput": true
},
{
"outputKey": "file",
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "4dff4173-e945-44ee-a450-4e01bd94eee2",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.has_file }}"
}
]
},
"renameOutput": true
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"typeVersion": 3
},
{
"id": "387a2288-194f-43eb-80a4-461ffe675758",
"name": "Telegram Trigger",
"type": "n8n-nodes-base.telegramTrigger",
"position": [
176,
560
],
"parameters": {
"updates": [
"message",
"callback_query"
],
"additionalFields": {}
},
"typeVersion": 1.1
},
{
"id": "abdc6eac-5b1b-44f1-8da9-5a78eb56a5fc",
"name": "Parse Message",
"type": "n8n-nodes-base.code",
"position": [
384,
560
],
"parameters": {
"jsCode": "const update = $input.first().json;\n\n// Handle callback_query (button tap)\nif (update.callback_query) {\n const cq = update.callback_query;\n return {\n json: {\n is_callback: true,\n has_file: false,\n command: '',\n callback_id: cq.id,\n callback_data: cq.data || '',\n chat_id: cq.message.chat.id,\n message_id: cq.message.message_id,\n username: cq.from.first_name || cq.from.username || 'there'\n }\n };\n}\n\n// Handle regular message\nconst msg = update.message;\nconst hasFile = msg.document !== undefined || msg.photo !== undefined;\nconst text = (msg.text || '').trim();\nconst chatId = msg.chat.id;\nconst username = msg.from.first_name || msg.from.username || 'there';\n\nlet command = '';\nlet args = '';\n\nif (!hasFile && text.startsWith('/')) {\n const parts = text.split(' ');\n command = parts[0].toLowerCase().replace(/@.*$/, '');\n args = parts.slice(1).join(' ').trim();\n}\n\nreturn {\n json: {\n is_callback: false,\n has_file: hasFile,\n command,\n args,\n text,\n chat_id: chatId,\n username,\n file_id: msg.document?.file_id || (msg.photo ? msg.photo[msg.photo.length - 1].file_id : null),\n filename: msg.document?.file_name || 'invoice.pdf',\n mime_type: msg.document?.mime_type || 'image/jpeg'\n }\n};"
},
"typeVersion": 2
},
{
"id": "f48db73a-00ea-42f4-80b9-09e6fddbea52",
"name": "Extract File Info",
"type": "n8n-nodes-base.code",
"position": [
832,
928
],
"parameters": {
"jsCode": "const data = $input.first().json;\nreturn {\n json: {\n file_id: data.file_id,\n filename: data.filename,\n mime_type: data.mime_type,\n chat_id: data.chat_id,\n username: data.username\n }\n};"
},
"typeVersion": 2
},
{
"id": "ed99dc61-6ff0-4d91-92a5-4c2747906d7e",
"name": "Download File",
"type": "n8n-nodes-base.telegram",
"position": [
1024,
928
],
"parameters": {
"fileId": "={{ $json.file_id }}",
"resource": "file",
"additionalFields": {}
},
"typeVersion": 1.2
},
{
"id": "0c36dfa0-859a-4fc6-a5ed-3a9004402752",
"name": "Prepare File",
"type": "n8n-nodes-base.code",
"position": [
1232,
928
],
"parameters": {
"jsCode": "const fileInfo = $('Extract File Info').first().json;\n\n// Read binary from filesystem (binaryDataMode: filesystem)\nconst binaryBuffer = await this.helpers.getBinaryDataBuffer(0, 'data');\nconst base64Data = binaryBuffer.toString('base64');\n\nreturn {\n json: {\n file_base64: base64Data,\n mime_type: fileInfo.mime_type,\n filename: fileInfo.filename,\n chat_id: fileInfo.chat_id,\n username: fileInfo.username,\n received_at: new Date().toISOString()\n }\n};"
},
"typeVersion": 2
},
{
"id": "a8e8d7f4-aa6b-4809-9c2c-0b46a3572134",
"name": "Build Request",
"type": "n8n-nodes-base.code",
"position": [
1392,
560
],
"parameters": {
"jsCode": "const d = $input.first().json;\nconst isPdf = d.mime_type === 'application/pdf';\n\nconst fileContent = isPdf\n ? {\n type: 'file',\n file: {\n filename: d.filename,\n file_data: `data:${d.mime_type};base64,${d.file_base64}`\n }\n }\n : {\n type: 'image_url',\n image_url: { url: `data:${d.mime_type};base64,${d.file_base64}` }\n };\n\nconst prompt = [\n 'You are a financial document parser for Indonesian stock broker trade confirmations.',\n '',\n 'Extract ALL transactions. Return ONLY a raw JSON array \u2014 no markdown, no code blocks, no commentary.',\n '',\n 'Output format (always an array):',\n '[{\"ticker\":\"BBCA\",\"company_name\":\"Bank Central Asia\",\"type\":\"buy\",\"quantity\":10,\"price\":8950,\"fee\":25000,\"total_amount\":8950000,\"transaction_date\":\"2024-02-15\",\"broker\":\"Mandiri Sekuritas\",\"confidence\":0.95}]',\n '',\n 'Rules:',\n '- ticker: uppercase, no .JK suffix',\n '- company_name: full company name as printed in the document. If not found, use \"unknown\"',\n '- type: \"buy\" (pembelian/beli) or \"sell\" (penjualan/jual)',\n '- quantity: LOTS only. 1 lot = 100 shares (lembar).',\n ' If document has a lot column \u2192 use it directly.',\n ' If document shows only shares \u2192 divide by 100.',\n ' Example: \"308 lot\" \u2192 308 | \"30.800 lembar\" \u2192 308. NEVER return share count.',\n '- price: price per share in IDR',\n '- fee: broker commission in IDR',\n '- total_amount: shares \u00d7 price in IDR (before fee)',\n '- transaction_date: YYYY-MM-DD',\n '- confidence: 0.0\u20131.0 (below 0.7 if uncertain)',\n '',\n `Document: ${d.filename}`\n].join('\\n');\n\nconst payload = {\n model: 'google/gemini-2.5-flash-lite',\n temperature: 0,\n max_tokens: 2048,\n messages: [{\n role: 'user',\n content: [{ type: 'text', text: prompt }, fileContent]\n }]\n};\n\nif (isPdf) {\n payload.plugins = [{ id: 'file-parser', pdf: { engine: 'mistral-ocr' } }];\n}\n\nreturn {\n json: {\n chat_id: d.chat_id,\n filename: d.filename,\n mime_type: d.mime_type,\n payload\n }\n};"
},
"typeVersion": 2
},
{
"id": "9e925f68-de8f-43b0-87b4-2fa26ee6fea3",
"name": "OpenRouter Extract",
"type": "n8n-nodes-base.httpRequest",
"position": [
1616,
560
],
"parameters": {
"url": "https://openrouter.ai/api/v1/chat/completions",
"method": "POST",
"options": {
"timeout": 30000
},
"jsonBody": "={{ $json.payload }}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"headerParameters": {
"parameters": [
{
"name": "HTTP-Referer",
"value": "https://n8n.io"
},
{
"name": "X-Title",
"value": "IDX Invoice Reader"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "0b0040e2-0827-44ad-947e-da4717ddf159",
"name": "Parse Invoice",
"type": "n8n-nodes-base.code",
"position": [
1824,
560
],
"parameters": {
"jsCode": "const resp = $input.first().json;\nconst chatId = $('Build Request').first().json.chat_id;\nconst filename = $('Build Request').first().json.filename;\n\nlet raw = '';\ntry {\n raw = resp.choices[0].message.content.trim();\n} catch(e) {\n return [{ json: { error: true, error_message: 'No response from LLM', chat_id: chatId, filename } }];\n}\n\n// Strip markdown code blocks if present\nraw = raw.replace(/^```json\\s*/i, '').replace(/^```\\s*/i, '').replace(/\\s*```$/i, '').trim();\n\nlet parsed;\ntry {\n parsed = JSON.parse(raw);\n} catch(e) {\n return [{ json: { error: true, error_message: e.message, raw_response: raw, chat_id: chatId, filename } }];\n}\n\n// Normalize to array\nconst transactions = Array.isArray(parsed) ? parsed : [parsed];\n\n// Return one item per transaction so Format Confirmation receives all\nreturn transactions.map(t => ({\n json: {\n ticker: (t.ticker || '').toUpperCase().replace(/\\.JK$/i, ''),\n company_name: t.company_name || 'unknown',\n type: t.type || 'buy',\n quantity: parseInt(t.quantity) || 0,\n price: parseFloat(t.price) || 0,\n fee: parseFloat(t.fee) || 0,\n total_amount: parseFloat(t.total_amount) || 0,\n transaction_date: t.transaction_date || '',\n broker: t.broker || '',\n confidence: parseFloat(t.confidence) || 0,\n chat_id: chatId,\n filename,\n total_in_batch: transactions.length\n }\n}));"
},
"typeVersion": 2
},
{
"id": "8f1b3f27-178c-475f-9878-5b4aaf0a48a1",
"name": "Invoice Error?",
"type": "n8n-nodes-base.if",
"position": [
2096,
992
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "a6faeb5d-74a6-4560-b020-3d1d50997cec",
"operator": {
"type": "boolean",
"operation": "exists",
"singleValue": true
},
"leftValue": "={{ $json.error }}",
"rightValue": true
}
]
}
},
"typeVersion": 2
},
{
"id": "04cd428b-e315-4d3a-91e0-3097e5c956e9",
"name": "Format Confirmation",
"type": "n8n-nodes-base.code",
"position": [
2320,
896
],
"parameters": {
"jsCode": "const items = $input.all();\nconst chatId = items[0].json.chat_id;\nconst filename = items[0].json.filename;\nconst broker = items[0].json.broker || 'Broker';\nconst date = items[0].json.transaction_date || '';\n\n// Store batch in workflow static data (persists between executions)\nconst staticData = $getWorkflowStaticData('global');\nconst batchId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\nstaticData[`batch_${batchId}`] = items.map(i => i.json);\n\n// Build transaction lines\nlet totalAmount = 0;\nlet totalFee = 0;\nconst lines = items.map((item, idx) => {\n const t = item.json;\n const lots = t.quantity;\n const typeEmoji = t.type === 'buy' ? '\ud83d\udfe2' : '\ud83d\udd34';\n const amount = t.total_amount || (lots * 100 * t.price);\n totalAmount += amount;\n totalFee += t.fee || 0;\n return `${idx + 1}. ${typeEmoji} *${t.ticker}* (${t.company_name}) ${lots} lots @ Rp ${Number(t.price).toLocaleString('id-ID')} = Rp ${Number(amount).toLocaleString('id-ID')}`;\n});\n\nconst confirmData = `confirm_batch|${batchId}`;\nconst cancelData = `cancel_batch|${batchId}`;\n\nreturn [{\n json: {\n chat_id: chatId,\n text: `\ud83d\udcc4 *Trade Confirmation \u2014 ${date}*\\n\ud83c\udfe6 ${broker}\\n\ud83d\udcc1 ${filename}\\n\\n${lines.join('\\n')}\\n\\n\ud83d\udcb8 Total: Rp ${Number(totalAmount).toLocaleString('id-ID')}\\n\ud83d\udcb3 Fee: Rp ${Number(totalFee).toLocaleString('id-ID')}\\n\\n*Save all ${items.length} transactions?*`,\n reply_markup: [\n { text: `\u2705 Save All (${items.length})`, callback_data: confirmData },\n { text: '\u274c Cancel', callback_data: cancelData }\n ]\n }\n}];"
},
"typeVersion": 2
},
{
"id": "b951be47-0a1d-4efc-b396-1906b9383a87",
"name": "Send Confirmation",
"type": "n8n-nodes-base.telegram",
"position": [
2560,
896
],
"parameters": {
"text": "={{ $json.text }}",
"chatId": "={{ $json.chat_id }}",
"additionalFields": {}
},
"typeVersion": 1.2
},
{
"id": "1a01e12b-4e49-4ccf-abcd-e56bbc892059",
"name": "Reply Invoice Error",
"type": "n8n-nodes-base.telegram",
"position": [
2336,
1088
],
"parameters": {
"text": "=\u274c Failed to read invoice: {{ $json.error_message }}\n\nPlease try again with a clearer PDF or image.",
"chatId": "={{ $json.chat_id }}",
"additionalFields": {}
},
"typeVersion": 1.2
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"versionId": "f2d57045-290d-412e-8f4c-fc9a881c07e2",
"connections": {
"Route Type1": {
"main": [
[],
[
{
"node": "Extract File Info",
"type": "main",
"index": 0
}
]
]
},
"Prepare File": {
"main": [
[
{
"node": "Build Request",
"type": "main",
"index": 0
}
]
]
},
"Build Request": {
"main": [
[
{
"node": "OpenRouter Extract",
"type": "main",
"index": 0
}
]
]
},
"Download File": {
"main": [
[
{
"node": "Prepare File",
"type": "main",
"index": 0
}
]
]
},
"Parse Invoice": {
"main": [
[
{
"node": "Invoice Error?",
"type": "main",
"index": 0
}
]
]
},
"Parse Message": {
"main": [
[
{
"node": "Route Type1",
"type": "main",
"index": 0
}
]
]
},
"Invoice Error?": {
"main": [
[
{
"node": "Reply Invoice Error",
"type": "main",
"index": 0
}
],
[
{
"node": "Format Confirmation",
"type": "main",
"index": 0
}
]
]
},
"Telegram Trigger": {
"main": [
[
{
"node": "Parse Message",
"type": "main",
"index": 0
}
]
]
},
"Extract File Info": {
"main": [
[
{
"node": "Download File",
"type": "main",
"index": 0
}
]
]
},
"OpenRouter Extract": {
"main": [
[
{
"node": "Parse Invoice",
"type": "main",
"index": 0
}
]
]
},
"Format Confirmation": {
"main": [
[
{
"node": "Send Confirmation",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Automatically extract stock transactions from Indonesian broker trade confirmation documents sent via Telegram using AI vision.
Source: https://n8n.io/workflows/14252/ — 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.
N8N Complete Final. Uses telegramTrigger, dataTable, telegram, mqtt. Event-driven trigger; 58 nodes.
TextMain. Uses telegramTrigger, stopAndError, telegram, httpRequest. Event-driven trigger; 56 nodes.
Pede Ai. Uses httpRequest, telegram, postgres, telegramTrigger. Event-driven trigger; 53 nodes.
📄 Documentation: Notion Guide
Telegram Wait. Uses stickyNote, httpRequest, redis, noOp. Event-driven trigger; 36 nodes.