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 →
{
"name": "MercadoChat - Fluxo 2: Processamento de Nota Fiscal",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "mercado-chat-receipt",
"responseMode": "lastNode",
"options": {}
},
"id": "webhook",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
250,
400
]
},
{
"parameters": {
"method": "GET",
"url": "=https://api.telegram.org/bot8733282592:AAF7232MFeMdRcV6HyHUeKvyTKe71cv8oNw/getFile?file_id={{ $json.photoFileId }}",
"sendHeaders": false,
"sendBody": false
},
"id": "get-file-path",
"name": "Obter Caminho do Arquivo (Telegram)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
450,
400
]
},
{
"parameters": {
"method": "GET",
"url": "=https://api.telegram.org/file/bot8733282592:AAF7232MFeMdRcV6HyHUeKvyTKe71cv8oNw/{{ $json.result.file_path }}",
"responseFormat": "file",
"sendHeaders": false,
"sendBody": false
},
"id": "download-image",
"name": "Download da Imagem",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
650,
400
]
},
{
"parameters": {
"jsCode": "const item = $input.first();\nconst binaryData = await this.helpers.getBinaryDataBuffer(0, 'data');\nconst base64Image = binaryData.toString('base64');\n\n// Prompt do arquivo 02_receipt_extractor.txt\nconst systemPrompt = `Analise a imagem da nota fiscal enviada pelo usu\u00e1rio e extraia os dados de compra.\\nRetorne APENAS um JSON puro, sem texto adicional, markdown ou explica\u00e7\u00f5es.\\n\\n## Instru\u00e7\u00f5es de extra\u00e7\u00e3o:\\n- Se n\u00e3o conseguir ler um campo com clareza, use null\\n- Pre\u00e7os devem ser n\u00fameros sem s\u00edmbolo de moeda (ex: 12.90)\\n- Datas no formato YYYY-MM-DD\\n- Quantidades como n\u00fameros (ex: 2, 1.5, 0.500)\\n- O nome do produto deve ser extra\u00eddo tal como aparece na NF\\n- CNPJ extraia apenas os n\u00fameros\\n\\n## Formato de resposta:\\n{\\n \\\"success\\\": true,\\n \\\"market_name\\\": \\\"<nome>\\\",\\n \\\"market_cnpj\\\": \\\"<cnpj>\\\",\\n \\\"market_address\\\": \\\"<endereco>\\\",\\n \\\"market_city\\\": \\\"<cidade>\\\",\\n \\\"market_state\\\": \\\"<UF>\\\",\\n \\\"market_cep\\\": \\\"<cep>\\\",\\n \\\"purchase_date\\\": \\\"<YYYY-MM-DD>\\\",\\n \\\"total\\\": <numero>,\\n \\\"items\\\": [ { \\\"name\\\": \\\"<nome>\\\", \\\"quantity\\\": <numero>, \\\"unit\\\": \\\"<unidade>\\\", \\\"unit_price\\\": <numero>, \\\"total_price\\\": <numero> } ]\\n}`;\n\nreturn [{ json: { base64Image, systemPrompt, originalData: $('Webhook').first().json.body } }];"
},
"id": "prepare-vision-prompt",
"name": "Preparar Base64 e Prompt",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
850,
400
]
},
{
"parameters": {
"method": "POST",
"url": "=https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=<redacted-credential>",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "=body",
"value": "={{ JSON.stringify({ contents: [{ parts: [{ text: $json.systemPrompt }, { inlineData: { mimeType: 'image/jpeg', data: $json.base64Image } }] }] }) }}"
}
]
}
},
"id": "gemini-vision",
"name": "Gemini Vision (Extrair NF)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1050,
400
]
},
{
"parameters": {
"jsCode": "const geminiResp = $input.first().json;\nconst prepData = $('Preparar Base64 e Prompt').first().json;\n\nlet receiptData = { success: false, error: 'Falha ao processar nota fiscal.' };\n\ntry {\n const rawText = geminiResp?.candidates?.[0]?.content?.parts?.[0]?.text || '{}';\n const cleanText = rawText.replace(/```json/g, '').replace(/```/g, '').trim();\n receiptData = JSON.parse(cleanText);\n} catch(e) {\n console.error('Erro no parse do Gemini', e);\n}\n\nreturn [{ json: { receipt: receiptData, userData: prepData.originalData, rawResponse: geminiResp?.candidates?.[0]?.content?.parts?.[0]?.text } }];"
},
"id": "parse-receipt",
"name": "Parse Dados Extra\u00eddos",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1250,
400
]
},
{
"parameters": {
"conditions": {
"options": {},
"conditions": [
{
"leftValue": "={{ $json.receipt.success }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true"
}
}
]
}
},
"id": "is-success",
"name": "Sucesso na Extra\u00e7\u00e3o?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1450,
300
]
},
{
"parameters": {
"chatId": "={{ $json.userData.chatId }}",
"text": "={{ '\u274c Ops! ' + ($json.receipt.error || 'N\u00e3o consegui ler essa imagem como uma nota fiscal v\u00e1lida. Pode tentar enviar uma foto mais clara?') }}",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"id": "send-error",
"name": "Enviar Erro de Leitura",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1650,
500
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Script robusto para salvar Market, Product, Prices, Purchase e Items no Supabase via API REST\n// Esta abordagem via Code node evita a complexidade de m\u00faltiplos n\u00f3s Supabase conectados que podem se perder na itera\u00e7\u00e3o\n\nconst receipt = $json.receipt;\nconst userData = $json.userData;\nconst supabaseUrl = 'https://lhybdghiuafcsvytxhdp.supabase.co';\nconst supabaseKey = '<redacted-credential>';\n\nasync function apiRequest(endpoint, method, body = null) {\n const options = {\n headers: {\n 'apikey': supabaseKey,\n 'Authorization': `Bearer ${supabaseKey}`,\n 'Content-Type': 'application/json',\n 'Prefer': 'return=representation'\n },\n method: method,\n url: `${supabaseUrl}/rest/v1/${endpoint}`,\n json: true\n };\n if (body) options.body = body;\n return await this.helpers.httpRequest(options);\n}\n\ntry {\n // 1. Processar Mercado (Upsert pelo CNPJ ou Nome)\n let marketId = null;\n if (receipt.market_cnpj || receipt.market_name) {\n const marketQ = receipt.market_cnpj ? `cnpj=eq.${receipt.market_cnpj}` : `name=eq.${encodeURIComponent(receipt.market_name)}`;\n const existingMarkets = await apiRequest(`markets?${marketQ}&select=id`, 'GET');\n \n if (existingMarkets && existingMarkets.length > 0) {\n marketId = existingMarkets[0].id;\n } else {\n const newMarket = await apiRequest(`markets`, 'POST', {\n name: receipt.market_name || 'Mercado Desconhecido',\n name_normalized: (receipt.market_name || '').toLowerCase().normalize('NFD').replace(/[\\u0300-\\u036f]/g, ''),\n cnpj: receipt.market_cnpj,\n address: receipt.market_address,\n city: receipt.market_city,\n state: receipt.market_state,\n cep: receipt.market_cep\n });\n if (newMarket && newMarket.length > 0) marketId = newMarket[0].id;\n }\n }\n \n // 2. Criar Compra (Purchase)\n let pointsEarned = 10; // Fixo para MVP (10 pts por cupom)\n const newPurchase = await apiRequest(`purchases`, 'POST', {\n user_id: userData.userId,\n market_id: marketId,\n total: receipt.total,\n purchase_date: receipt.purchase_date || new Date().toISOString().split('T')[0],\n points_earned: pointsEarned,\n status: 'processed'\n });\n const purchaseId = newPurchase[0].id;\n\n // 3. Processar Itens\n for (const item of (receipt.items || [])) {\n if (!item.name) continue;\n \n const normalizedName = item.name.toLowerCase().normalize('NFD').replace(/[\\u0300-\\u036f]/g, '');\n \n // Tenta achar produto\n const existingProducts = await apiRequest(`products?name_normalized=eq.${encodeURIComponent(normalizedName)}&select=id`, 'GET');\n let productId = null;\n \n if (existingProducts && existingProducts.length > 0) {\n productId = existingProducts[0].id;\n } else {\n const newProduct = await apiRequest(`products`, 'POST', {\n name: item.name,\n name_normalized: normalizedName,\n unit: item.unit\n });\n if (newProduct && newProduct.length > 0) productId = newProduct[0].id;\n }\n \n // Inserir Item da compra\n await apiRequest(`purchase_items`, 'POST', {\n purchase_id: purchaseId,\n product_id: productId,\n product_name_raw: item.name,\n quantity: item.quantity || 1,\n unit_price: item.unit_price,\n total_price: item.total_price\n });\n \n // Inserir Price Record\n if (productId && marketId && item.unit_price) {\n await apiRequest(`price_records`, 'POST', {\n product_id: productId,\n market_id: marketId,\n price: item.unit_price,\n quantity: item.quantity || 1,\n unit: item.unit,\n recorded_at: receipt.purchase_date ? new Date(receipt.purchase_date).toISOString() : new Date().toISOString(),\n source: 'receipt'\n });\n }\n }\n\n // 4. Atualizar Pontos do Usu\u00e1rio\n const newPoints = parseInt(userData.userPoints || 0) + pointsEarned;\n await apiRequest(`users?id=eq.${userData.userId}`, 'PATCH', { points: newPoints });\n \n // Criar hist\u00f3rico de pontos\n await apiRequest(`points_history`, 'POST', {\n user_id: userData.userId,\n points: pointsEarned,\n description: 'Envio de Nota Fiscal',\n purchase_id: purchaseId\n });\n\n return [{ json: { success: true, pointsEarned, newPoints, itemCount: (receipt.items || []).length, marketName: receipt.market_name, chatId: userData.chatId } }];\n \n} catch (error) {\n console.error('Erro ao salvar no DB:', error);\n return [{ json: { success: false, error: error.message, chatId: userData.chatId } }];\n}"
},
"id": "save-to-supabase",
"name": "Salvar Dados no Supabase (C\u00f3digo)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1650,
200
]
},
{
"parameters": {
"conditions": {
"options": {},
"conditions": [
{
"leftValue": "={{ $json.success }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true"
}
}
]
}
},
"id": "is-db-success",
"name": "Sucesso no Banco?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1850,
200
]
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ '\u2705 *Nota Fiscal lida com sucesso!* \ud83c\udf89\\n\\nEstabelecimento: *' + ($json.marketName || 'Desconhecido') + '*\\nItens registrados: *' + $json.itemCount + '*\\n\\n\ud83c\udf81 Voc\u00ea ganhou *+' + $json.pointsEarned + ' pontos* e agora tem um total de *' + $json.newPoints + ' pontos*!\\n\\nOs pre\u00e7os j\u00e1 foram atualizados no sistema e v\u00e3o ajudar outros compradores. Valeu! \ud83d\uded2' }}",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"id": "send-success",
"name": "Enviar Confirma\u00e7\u00e3o e Pontos",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2050,
100
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "\u274c Falha ao processar os dados da sua nota fiscal no nosso sistema. Por favor, tente novamente mais tarde.",
"additionalFields": {}
},
"id": "send-db-error",
"name": "Enviar Erro de Sistema",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2050,
300
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
}
],
"connections": {
"Execute Workflow Trigger": {
"main": [
[
{
"node": "Obter Caminho do Arquivo (Telegram)",
"type": "main",
"index": 0
}
]
]
},
"Obter Caminho do Arquivo (Telegram)": {
"main": [
[
{
"node": "Download da Imagem",
"type": "main",
"index": 0
}
]
]
},
"Download da Imagem": {
"main": [
[
{
"node": "Preparar Base64 e Prompt",
"type": "main",
"index": 0
}
]
]
},
"Preparar Base64 e Prompt": {
"main": [
[
{
"node": "Gemini Vision (Extrair NF)",
"type": "main",
"index": 0
}
]
]
},
"Gemini Vision (Extrair NF)": {
"main": [
[
{
"node": "Parse Dados Extra\u00eddos",
"type": "main",
"index": 0
}
]
]
},
"Parse Dados Extra\u00eddos": {
"main": [
[
{
"node": "Sucesso na Extra\u00e7\u00e3o?",
"type": "main",
"index": 0
}
]
]
},
"Sucesso na Extra\u00e7\u00e3o?": {
"main": [
[
{
"node": "Salvar Dados no Supabase (C\u00f3digo)",
"type": "main",
"index": 0
}
],
[
{
"node": "Enviar Erro de Leitura",
"type": "main",
"index": 0
}
]
]
},
"Salvar Dados no Supabase (C\u00f3digo)": {
"main": [
[
{
"node": "Sucesso no Banco?",
"type": "main",
"index": 0
}
]
]
},
"Sucesso no Banco?": {
"main": [
[
{
"node": "Enviar Confirma\u00e7\u00e3o e Pontos",
"type": "main",
"index": 0
}
],
[
{
"node": "Enviar Erro de Sistema",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"tags": [
"mercado-chat",
"sub-workflow"
],
"triggerCount": 0,
"updatedAt": "2026-03-06T00:00:00.000Z",
"versionId": "2"
}
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.
telegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
MercadoChat - Fluxo 2: Processamento de Nota Fiscal. Uses httpRequest, telegram. Webhook trigger; 12 nodes.
Source: https://github.com/Shtorache/mercadinho_chat/blob/f596a308e1f9f899d9a51a3df1ece34e9b5b9b34/n8n_workflows/02_fluxo_nota_fiscal.json — 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.
qualiopi. Uses airtable, telegram, emailSend, httpRequest. Webhook trigger; 51 nodes.
PsyCardv2. Uses executeCommand, telegram, readBinaryFile, googleDrive. Webhook trigger; 41 nodes.
[](https://www.linkedin.com/in/mosaab-yassir-lafrimi/)[](https://t.me/joevenner)
How it works • Webhook triggers from content creation system in Airtable • Downloads media (images/videos) from Airtable URLs • Uploads media to Postiz cloud storage • Schedules or publishes content a
I wanted to avoid the rush at end of month to log expenses. I tried existing expense apps but found them either too expensive for what they offer, or frustrating with inconsistent extraction results.