This workflow corresponds to n8n.io template #16267 — we link there as the canonical source.
This workflow follows the Gmail → Google Sheets 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": "ZNYXK9s05gWFqmB1",
"name": "Vendor Invoice Duplicate Payment Blocker",
"tags": [
{
"id": "2V3HXFbv2wqNGm6s",
"name": "Dev",
"createdAt": "2025-06-17T05:42:41.949Z",
"updatedAt": "2025-06-17T05:42:41.949Z"
}
],
"nodes": [
{
"id": "dc22bf33-0f6c-44fa-b613-325f937e5178",
"name": "Invoice Intake Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
-1376,
464
],
"parameters": {
"path": "ap/invoice-intake",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 2
},
{
"id": "8ccc87af-c692-4e4f-9422-547d2392c3df",
"name": "Parse Invoice Payload",
"type": "n8n-nodes-base.code",
"position": [
-1152,
464
],
"parameters": {
"jsCode": "// ================================================\n// INVOICE PAYLOAD PARSER & NORMALIZER\n// Builds a fingerprint used for duplicate detection\n// ================================================\n\nconst body = $input.first().json;\nconst raw = Array.isArray(body) ? body : [body];\n\nconst out = [];\n\nfor (const inv of raw) {\n const data = inv.invoice || inv;\n\n const vendorName = String(data.vendor_name || data.supplier || '').trim();\n const vendorId = String(data.vendor_id || data.supplier_id || '').trim();\n const invoiceNumber = String(data.invoice_number || data.invoice_no || data.number || '').trim();\n const amount = Number(data.amount || data.total || data.grand_total || 0);\n const currency = String(data.currency || 'USD').toUpperCase();\n const invoiceDate = String(data.invoice_date || data.date || '').trim();\n const poNumber = String(data.po_number || data.purchase_order || '').trim();\n\n // Skip records missing the minimum identifying fields\n if (!vendorName && !vendorId) {\n console.log('Skipping invoice: no vendor identity');\n continue;\n }\n if (!invoiceNumber || amount <= 0) {\n console.log('Skipping invoice: missing number or amount');\n continue;\n }\n\n // Normalized fingerprint: vendor + invoice number + amount (rounded to cents)\n const normVendor = (vendorId || vendorName).toLowerCase().replace(/[^a-z0-9]/g, '');\n const normNumber = invoiceNumber.toLowerCase().replace(/[^a-z0-9]/g, '');\n const normAmount = amount.toFixed(2);\n const fingerprint = normVendor + '|' + normNumber + '|' + normAmount;\n\n out.push({\n json: {\n receivedAt: new Date().toISOString(),\n vendorName,\n vendorId,\n invoiceNumber,\n amount,\n currency,\n invoiceDate,\n poNumber,\n submittedBy: String(data.submitted_by || data.email || '').toLowerCase().trim(),\n sourceChannel: String(data.source || 'webhook'),\n fingerprint,\n _parseError: false\n }\n });\n}\n\nif (out.length === 0) {\n return [{ json: { _parseError: true, fingerprint: '', invoiceNumber: '', amount: 0 } }];\n}\n\nreturn out;"
},
"typeVersion": 2
},
{
"id": "6244bc46-0b63-4412-9ae0-1461617af1a2",
"name": "Is Valid Invoice",
"type": "n8n-nodes-base.if",
"position": [
-928,
464
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "check-no-error",
"operator": {
"type": "boolean",
"operation": "notEquals"
},
"leftValue": "={{ $json._parseError }}",
"rightValue": true
},
{
"id": "check-fingerprint",
"operator": {
"type": "string",
"operation": "notEquals"
},
"leftValue": "={{ $json.fingerprint }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2
},
{
"id": "798b98dd-8df2-4bfd-8c48-932f5588e8e9",
"name": "Lookup Existing Invoice",
"type": "n8n-nodes-base.googleSheets",
"position": [
-704,
464
],
"parameters": {
"operation": "lookup",
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_GOOGLE_SHEET_ID"
}
},
"typeVersion": 4.5,
"alwaysOutputData": true
},
{
"id": "c6fa6f1a-1ef5-463f-b23a-b8263ecb2724",
"name": "Flag Duplicate Status",
"type": "n8n-nodes-base.set",
"position": [
-480,
464
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "a-isdup",
"name": "isDuplicate",
"type": "boolean",
"value": "={{ $json.fingerprint ? true : false }}"
},
{
"id": "a-matched-date",
"name": "matchedPaidDate",
"type": "string",
"value": "={{ $json.paid_date || '' }}"
},
{
"id": "a-matched-ref",
"name": "matchedPaymentRef",
"type": "string",
"value": "={{ $json.payment_ref || '' }}"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "3e5e3060-075e-4991-ac51-0ef1253431f4",
"name": "Duplicate Found",
"type": "n8n-nodes-base.if",
"position": [
-256,
464
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "check-dup",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.isDuplicate }}",
"rightValue": true
}
]
}
},
"typeVersion": 2
},
{
"id": "1665372c-e945-46e9-8ad2-c4ac2cbe61ef",
"name": "Slack Duplicate Alert",
"type": "n8n-nodes-base.slack",
"position": [
-32,
368
],
"parameters": {
"text": "=:rotating_light: DUPLICATE INVOICE BLOCKED - Payment Held\n\nA matching invoice was already paid. This submission has been blocked pending review.\n\nVendor: {{ $('Parse Invoice Payload').item.json.vendorName }}\nInvoice Number: {{ $('Parse Invoice Payload').item.json.invoiceNumber }}\nAmount: {{ $('Parse Invoice Payload').item.json.currency }} {{ $('Parse Invoice Payload').item.json.amount }}\nPO Number: {{ $('Parse Invoice Payload').item.json.poNumber || 'N/A' }}\nSubmitted By: {{ $('Parse Invoice Payload').item.json.submittedBy || 'N/A' }}\n\nOriginal Payment Date: {{ $json.matchedPaidDate || 'Unknown' }}\nOriginal Payment Ref: {{ $json.matchedPaymentRef || 'Unknown' }}\n\nAction: Verify with AP before approving. Do NOT release payment until confirmed unique.",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": "YOUR_SLACK_CHANNEL_ID"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"typeVersion": 2.2
},
{
"id": "30fc85b7-8332-43bb-b53a-7483e55a98c6",
"name": "Log Duplicate",
"type": "n8n-nodes-base.googleSheets",
"position": [
192,
368
],
"parameters": {
"columns": {
"value": {
"amount": "={{ $('Parse Invoice Payload').item.json.amount }}",
"status": "BLOCKED_DUPLICATE",
"currency": "={{ $('Parse Invoice Payload').item.json.currency }}",
"loggedAt": "={{ $now.toISO() }}",
"vendorName": "={{ $('Parse Invoice Payload').item.json.vendorName }}",
"fingerprint": "={{ $('Parse Invoice Payload').item.json.fingerprint }}",
"submittedBy": "={{ $('Parse Invoice Payload').item.json.submittedBy }}",
"invoiceNumber": "={{ $('Parse Invoice Payload').item.json.invoiceNumber }}",
"originalPaymentRef": "={{ $('Flag Duplicate Status').item.json.matchedPaymentRef }}"
},
"mappingMode": "defineBelow"
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "DuplicateLog",
"cachedResultName": "DuplicateLog"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_GOOGLE_SHEET_ID"
}
},
"typeVersion": 4.5
},
{
"id": "3fbe859e-b953-48b7-b98d-ae9e7b6267da",
"name": "Prepare Approval",
"type": "n8n-nodes-base.set",
"position": [
-32,
560
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "a-approval-status",
"name": "approvalStatus",
"type": "string",
"value": "={{ $('Parse Invoice Payload').item.json.amount >= 5000 ? 'NEEDS_MANAGER_APPROVAL' : 'AUTO_APPROVED' }}"
},
{
"id": "a-due",
"name": "paymentDueISO",
"type": "string",
"value": "={{ $now.plus({days: 14}).toISO() }}"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "99784191-af45-4bf2-b40c-096eccd9c537",
"name": "Register Unique Invoice",
"type": "n8n-nodes-base.googleSheets",
"position": [
192,
560
],
"parameters": {
"columns": {
"value": {
"amount": "={{ $('Parse Invoice Payload').item.json.amount }}",
"currency": "={{ $('Parse Invoice Payload').item.json.currency }}",
"poNumber": "={{ $('Parse Invoice Payload').item.json.poNumber }}",
"paid_date": "",
"vendorName": "={{ $('Parse Invoice Payload').item.json.vendorName }}",
"fingerprint": "={{ $('Parse Invoice Payload').item.json.fingerprint }}",
"payment_ref": "",
"registeredAt": "={{ $now.toISO() }}",
"invoiceNumber": "={{ $('Parse Invoice Payload').item.json.invoiceNumber }}",
"approvalStatus": "={{ $json.approvalStatus }}"
},
"mappingMode": "defineBelow"
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "PaidInvoices",
"cachedResultName": "PaidInvoices"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_GOOGLE_SHEET_ID"
}
},
"typeVersion": 4.5
},
{
"id": "355d13ab-03a6-4853-b962-3d5b7da3f862",
"name": "Needs Approval",
"type": "n8n-nodes-base.if",
"position": [
416,
560
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "check-approval",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $('Prepare Approval').item.json.approvalStatus }}",
"rightValue": "NEEDS_MANAGER_APPROVAL"
}
]
}
},
"typeVersion": 2
},
{
"id": "aad05f25-e543-4ec4-bc72-f5283f43d93b",
"name": "Email Approver",
"type": "n8n-nodes-base.gmail",
"position": [
640,
464
],
"parameters": {
"sendTo": "YOUR_APPROVER_EMAIL",
"message": "=An invoice above the auto-approval threshold has cleared the duplicate check and needs your sign off.\n\nVendor: {{ $('Parse Invoice Payload').item.json.vendorName }}\nInvoice Number: {{ $('Parse Invoice Payload').item.json.invoiceNumber }}\nAmount: {{ $('Parse Invoice Payload').item.json.currency }} {{ $('Parse Invoice Payload').item.json.amount }}\nPO Number: {{ $('Parse Invoice Payload').item.json.poNumber || 'N/A' }}\nInvoice Date: {{ $('Parse Invoice Payload').item.json.invoiceDate || 'N/A' }}\nPayment Due: {{ $('Prepare Approval').item.json.paymentDueISO }}\n\nThis invoice has been confirmed unique and registered. Approve or reject in the AP system.",
"options": {},
"subject": "=Invoice Approval Needed: {{ $('Parse Invoice Payload').item.json.vendorName }} - {{ $('Parse Invoice Payload').item.json.currency }} {{ $('Parse Invoice Payload').item.json.amount }}"
},
"typeVersion": 2.1
},
{
"id": "8ee39ec8-3df6-408f-b144-873c90b87ffa",
"name": "Slack Cleared Notice",
"type": "n8n-nodes-base.slack",
"position": [
640,
656
],
"parameters": {
"text": "=:white_check_mark: Invoice Cleared - No Duplicate Found\n\nVendor: {{ $('Parse Invoice Payload').item.json.vendorName }}\nInvoice Number: {{ $('Parse Invoice Payload').item.json.invoiceNumber }}\nAmount: {{ $('Parse Invoice Payload').item.json.currency }} {{ $('Parse Invoice Payload').item.json.amount }}\nStatus: {{ $('Prepare Approval').item.json.approvalStatus }}\nPayment Due: {{ $('Prepare Approval').item.json.paymentDueISO }}\n\nThe invoice has been registered as unique and is safe to process.",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": "YOUR_SLACK_CHANNEL_ID"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"typeVersion": 2.2
},
{
"id": "ccdd9d59-2064-42b3-a4e5-6a6d7b0b8516",
"name": "Respond to Caller",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
864,
560
],
"parameters": {
"options": {},
"respondWith": "json",
"responseBody": "={{ JSON.stringify({ status: 'processed', invoiceNumber: $('Parse Invoice Payload').item.json.invoiceNumber, duplicate: $('Flag Duplicate Status').item.json.isDuplicate }) }}"
},
"typeVersion": 1.1
},
{
"id": "6578037c-d19a-4d90-8eb9-07e73525d2a0",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1984,
144
],
"parameters": {
"width": 496,
"height": 704,
"content": "## Vendor Invoice Duplicate Payment Blocker\nThis workflow stops finance teams from paying the same vendor invoice twice. Every incoming invoice is normalized into a unique fingerprint and checked against a register of already-paid invoices. Duplicates are blocked, alerted, and logged for audit, while genuinely new invoices are registered, routed for approval if large, and confirmed as safe to pay.\n\n### How it Works\n\n\t- Receives invoice data from the AP intake webhook.\n\t- Parses and normalizes vendor, number, and amount into a fingerprint.\n\t- Validates that required fields are present before continuing.\n\t- Looks up the fingerprint against the paid-invoice register.\n\t- Flags whether the invoice is a duplicate.\n\t- Blocks duplicates, alerts the team, and logs them for audit.\n\t- Registers unique invoices and sets an approval status.\n\t- Routes high-value invoices to a manager for email approval.\n\t- Confirms cleared invoices to the team and responds to the caller.\n\n### Setup Steps\n\n\t1. Connect the AP system or OCR tool to post invoices to the webhook.\n\t2. Create a Google Sheet with PaidInvoices, DuplicateLog tabs and a fingerprint column.\n\t3. Add your Google Sheets credential and set the sheet ID.\n\t4. Add your Slack credential and target channel ID.\n\t5. Add your Gmail credential and the approver email address.\n\t6. Adjust the approval threshold amount if needed.\n\t7. Activate the workflow so it runs automatically."
},
"typeVersion": 1
},
{
"id": "21afb723-b98b-418e-bef3-f500749aaba3",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1472,
144
],
"parameters": {
"color": 7,
"width": 696,
"height": 704,
"content": "## Step 1: Receive and Validate Invoice\n\nThis section captures incoming invoices from the AP intake webhook and normalizes the key fields into a unique fingerprint. It then validates that the vendor, invoice number, and amount are present so only complete records move forward."
},
"typeVersion": 1
},
{
"id": "81507182-6e60-4b99-bf34-1cf4da91e313",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-752,
144
],
"parameters": {
"color": 7,
"width": 648,
"height": 704,
"content": "## Step 2: Detect Duplicates\n\nThis section looks up the invoice fingerprint against the register of already-paid invoices and flags whether it is a duplicate. Confirmed duplicates are alerted to the team on Slack and logged to an audit sheet so the payment is held."
},
"typeVersion": 1
},
{
"id": "738c1027-360d-4ae7-9e53-9d1834ba7aba",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-80,
144
],
"parameters": {
"color": 7,
"width": 1136,
"height": 704,
"content": "## Step 3: Register, Approve and Notify\n\nUnique invoices are registered in the paid-invoice sheet and given an approval status based on amount. High-value invoices are emailed to a manager for sign off, cleared invoices are announced on Slack, and the caller receives a final response."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "b1de899d-14cb-4211-ab7a-58b9a5544a51",
"connections": {
"Email Approver": {
"main": [
[
{
"node": "Respond to Caller",
"type": "main",
"index": 0
}
]
]
},
"Needs Approval": {
"main": [
[
{
"node": "Email Approver",
"type": "main",
"index": 0
}
],
[
{
"node": "Slack Cleared Notice",
"type": "main",
"index": 0
}
]
]
},
"Duplicate Found": {
"main": [
[
{
"node": "Slack Duplicate Alert",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Approval",
"type": "main",
"index": 0
}
]
]
},
"Is Valid Invoice": {
"main": [
[
{
"node": "Lookup Existing Invoice",
"type": "main",
"index": 0
}
]
]
},
"Prepare Approval": {
"main": [
[
{
"node": "Register Unique Invoice",
"type": "main",
"index": 0
}
]
]
},
"Slack Cleared Notice": {
"main": [
[
{
"node": "Respond to Caller",
"type": "main",
"index": 0
}
]
]
},
"Flag Duplicate Status": {
"main": [
[
{
"node": "Duplicate Found",
"type": "main",
"index": 0
}
]
]
},
"Parse Invoice Payload": {
"main": [
[
{
"node": "Is Valid Invoice",
"type": "main",
"index": 0
}
]
]
},
"Slack Duplicate Alert": {
"main": [
[
{
"node": "Log Duplicate",
"type": "main",
"index": 0
}
]
]
},
"Invoice Intake Webhook": {
"main": [
[
{
"node": "Parse Invoice Payload",
"type": "main",
"index": 0
}
]
]
},
"Lookup Existing Invoice": {
"main": [
[
{
"node": "Flag Duplicate Status",
"type": "main",
"index": 0
}
]
]
},
"Register Unique Invoice": {
"main": [
[
{
"node": "Needs Approval",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow receives vendor invoices via a webhook, normalizes them into a fingerprint, checks Google Sheets for previously paid matches, and then blocks duplicates with Slack alerts and an audit log while registering unique invoices and optionally requesting manager approval…
Source: https://n8n.io/workflows/16267/ — 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.
Complete Calendly automation that handles confirmations, cancellations and reschedules in a single workflow. WHAT IT DOES:
Who is this for? This template is ideal for event organizers, conference managers, and community teams who need an automated participant management system. Perfect for workshops, conferences, meetups,
Automatically generate, validate, and deliver professional course completion certificates with zero manual work — from webhook request to PDF delivery in seconds.
Streamline and standardize your entire client onboarding process with a single end-to-end automation. 🚀📋 This workflow captures detailed client intake data via webhook, automatically creates a fully s
This workflow accepts new-hire details via a webhook, logs them to Google Sheets, creates an onboarding folder in Google Drive, opens onboarding work items in Jira, and notifies the employee and inter