Manual invoice processing costs businesses an average of $15 per invoice. With AI, that drops to under $2.
This guide shows you exactly how to build an automated invoice processing system—from email to your accounting software.
What We’re Building
[Invoice Email] → [Extract PDF] → [AI Data Extraction] → [Validation]
↓
[Create Accounting Entry] → [Alert if Anomaly]
End Result: Invoices arrive by email, get processed automatically, and appear in your accounting software—with human review only for exceptions.
Prerequisites
- n8n (self-hosted or cloud) — see our beginner tutorial if you’re new to n8n
- Anthropic API key (Claude)
- Email account (Gmail, Outlook, etc.)
- Accounting software with API (QuickBooks, Xero, FreshBooks)
- 30 minutes to set this up
Step 1: Email Trigger Setup
First, we’ll watch for incoming invoices.
n8n Email Trigger Configuration
- Add an Email Trigger (IMAP) node
- Configure your email credentials
- Set up a filter:
- Subject contains: “invoice” OR “payment due” OR “bill”
- Or create a dedicated invoices@ email address
Polling Interval: Every 5 minutes
Folder: INBOX (or dedicated folder)
Mark as Read: After processing
Pro Tip: Create a Gmail filter that auto-labels emails with attachments containing “invoice” in the filename. Then trigger on that label.
Step 2: Extract PDF Attachment
Add a Move Binary Data node to isolate the PDF:
Mode: Array to Fields
Data Property: attachments
Then use an IF node to filter:
- File extension is PDF
- File size > 10KB (filters out signatures, icons)
Step 3: AI Data Extraction (The Magic)
This is where Claude reads the invoice and extracts structured data.
Anthropic Node Configuration
Model: claude-3-5-sonnet-20241022 (best balance of speed and accuracy)
System Prompt:
You are an invoice data extraction specialist. Extract information from invoice images/PDFs and return structured JSON.
Always extract:
- vendor_name: Company name on the invoice
- vendor_address: Full address if available
- invoice_number: Unique invoice identifier
- invoice_date: Date of invoice (ISO format)
- due_date: Payment due date (ISO format)
- subtotal: Amount before tax
- tax_amount: Tax amount (0 if none)
- total_amount: Total due
- currency: Three-letter code (USD, EUR, etc.)
- line_items: Array of {description, quantity, unit_price, amount}
- payment_terms: Net 30, Due on Receipt, etc.
- bank_details: If provided for payment
If a field cannot be determined, use null.
Return ONLY valid JSON, no explanation.
User Message:
Extract invoice data from this document:
{{$binary.data}}
Response Format: JSON
Sample Output
{
"vendor_name": "Acme Software Inc",
"vendor_address": "123 Tech Street, San Francisco, CA 94105",
"invoice_number": "INV-2025-0142",
"invoice_date": "2025-01-15",
"due_date": "2025-02-14",
"subtotal": 1500.00,
"tax_amount": 142.50,
"total_amount": 1642.50,
"currency": "USD",
"line_items": [
{
"description": "Monthly SaaS subscription",
"quantity": 1,
"unit_price": 1500.00,
"amount": 1500.00
}
],
"payment_terms": "Net 30",
"bank_details": null
}
Step 4: Data Validation
Before creating an accounting entry, validate the extracted data.
Validation Rules
Add a Code node with validation logic:
const invoice = $json;
const errors = [];
// Required fields check
if (!invoice.vendor_name) errors.push("Missing vendor name");
if (!invoice.total_amount) errors.push("Missing total amount");
if (!invoice.invoice_number) errors.push("Missing invoice number");
// Sanity checks
if (invoice.total_amount > 50000) {
errors.push(`Unusually high amount: ${invoice.total_amount}`);
}
if (invoice.tax_amount && invoice.subtotal) {
const expectedTax = invoice.subtotal * 0.15; // Adjust for your tax rate
if (Math.abs(invoice.tax_amount - expectedTax) > expectedTax * 0.5) {
errors.push("Tax amount seems incorrect");
}
}
// Check for duplicate invoice number (query your database)
// const isDuplicate = await checkDuplicate(invoice.invoice_number);
// if (isDuplicate) errors.push("Duplicate invoice number");
return {
...invoice,
validation_errors: errors,
needs_review: errors.length > 0
};
Step 5: Vendor Matching
Match the invoice vendor to your existing vendor list.
Fuzzy Matching Logic
const extractedVendor = $json.vendor_name.toLowerCase();
const existingVendors = $('Get Vendors').item.json.vendors; // From your accounting software
// Simple fuzzy match
function similarity(s1, s2) {
const longer = s1.length > s2.length ? s1 : s2;
const shorter = s1.length > s2.length ? s2 : s1;
if (longer.length === 0) return 1.0;
const costs = [];
for (let i = 0; i <= shorter.length; i++) {
let lastValue = i;
for (let j = 0; j <= longer.length; j++) {
if (i === 0) costs[j] = j;
else if (j > 0) {
let newValue = costs[j - 1];
if (shorter[i - 1] !== longer[j - 1]) {
newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1;
}
costs[j - 1] = lastValue;
lastValue = newValue;
}
}
if (i > 0) costs[longer.length] = lastValue;
}
return (longer.length - costs[longer.length]) / longer.length;
}
const matches = existingVendors
.map(v => ({
vendor: v,
score: similarity(extractedVendor, v.name.toLowerCase())
}))
.filter(m => m.score > 0.7)
.sort((a, b) => b.score - a.score);
return {
...$json,
matched_vendor: matches[0]?.vendor || null,
vendor_confidence: matches[0]?.score || 0,
needs_vendor_review: !matches[0] || matches[0].score < 0.9
};
Step 6: Create Accounting Entry
Now push to your accounting software.
QuickBooks Online Example
Use the HTTP Request node:
Method: POST
URL: https://quickbooks.api.intuit.com/v3/company/{{companyId}}/bill
Headers:
Authorization: Bearer {{$credentials.quickbooks.accessToken}}
Content-Type: application/json
Body:
{
"VendorRef": {
"value": "{{$json.matched_vendor.id}}"
},
"Line": [
{{#each line_items}}
{
"Amount": {{this.amount}},
"DetailType": "AccountBasedExpenseLineDetail",
"AccountBasedExpenseLineDetail": {
"AccountRef": {
"value": "{{../default_expense_account}}"
}
},
"Description": "{{this.description}}"
}{{#unless @last}},{{/unless}}
{{/each}}
],
"DueDate": "{{$json.due_date}}",
"TxnDate": "{{$json.invoice_date}}",
"DocNumber": "{{$json.invoice_number}}"
}
Xero Example
Method: POST
URL: https://api.xero.com/api.xro/2.0/Invoices
Body:
{
"Type": "ACCPAY",
"Contact": {
"ContactID": "{{$json.matched_vendor.xero_id}}"
},
"Date": "{{$json.invoice_date}}",
"DueDate": "{{$json.due_date}}",
"InvoiceNumber": "{{$json.invoice_number}}",
"LineItems": [
{{#each line_items}}
{
"Description": "{{this.description}}",
"Quantity": {{this.quantity}},
"UnitAmount": {{this.unit_price}},
"AccountCode": "400"
}{{#unless @last}},{{/unless}}
{{/each}}
]
}
Step 7: Exception Handling
Not every invoice will process cleanly. Build exception workflows.
Exception Types
- Missing Data: AI couldn’t extract required fields
- Validation Failure: Data looks wrong
- Unknown Vendor: No match in system
- High Value: Needs approval before entry
- Duplicate: Invoice number already exists
Exception Workflow
[Validation Errors?] → YES → [Create Review Task]
↓
[Slack Alert]
↓
[Store for Manual Processing]
Slack Alert Format
🔍 *Invoice Needs Review*
*Vendor:* {{$json.vendor_name}}
*Amount:* {{$json.currency}} {{$json.total_amount}}
*Invoice #:* {{$json.invoice_number}}
*Issues:*
{{#each validation_errors}}
• {{this}}
{{/each}}
<review_link|Review Invoice>
Step 8: Audit Trail
Keep records of everything for compliance.
Log Every Processing Step
// Create audit log entry
const auditEntry = {
timestamp: new Date().toISOString(),
invoice_number: $json.invoice_number,
vendor: $json.vendor_name,
amount: $json.total_amount,
source_email: $('Email Trigger').item.json.from,
processing_status: $json.needs_review ? 'manual_review' : 'auto_processed',
validation_errors: $json.validation_errors,
created_entry_id: $json.accounting_entry_id,
pdf_storage_path: $json.pdf_path
};
// Store in your database or Google Sheets
return auditEntry;
Complete Workflow Overview
1. Email Trigger (Every 5 min)
↓
2. Filter for PDFs
↓
3. Claude: Extract Data
↓
4. Validate Data
↓
5. Match Vendor
↓
6. [Branch]
├── Clean → Create Entry → Store PDF → Log
└── Issues → Create Review Task → Alert → Log
Performance Results
After implementing this for clients:
| Metric | Before | After |
|---|---|---|
| Processing time per invoice | 5-10 min | 30 sec |
| Error rate | 3-5% | <1% (with AI extraction) |
| Cost per invoice | $12-15 | $1-2 |
| Monthly hours saved | 15-20 hrs | - |
Common Issues & Solutions
AI Extraction Accuracy
Problem: Claude misreads handwritten or low-quality scans.
Solution: Add image preprocessing (increase contrast, deskew) or fall back to manual for low-confidence extractions.
Vendor Name Variations
Problem: Same vendor, different names (“ACME Inc” vs “Acme Incorporated”).
Solution: Build a vendor alias table. Map variations to canonical vendor IDs.
Currency Handling
Problem: International invoices with different currencies.
Solution: Extract currency code and either convert or flag for manual handling if not your base currency.
Duplicate Invoices
Problem: Same invoice forwarded multiple times.
Solution: Check invoice_number + vendor + amount combination before creating entry.
Extending the System
Once basic processing works, consider:
- Approval Workflows: Route high-value invoices for approval
- Early Payment Optimization: Flag invoices with early payment discounts
- Spend Analytics: Dashboard of spending by vendor/category
- Payment Automation: Trigger payments when due
Need help setting this up for your specific accounting software? Book a free consultation.