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

  1. Add an Email Trigger (IMAP) node
  2. Configure your email credentials
  3. 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

  1. Missing Data: AI couldn’t extract required fields
  2. Validation Failure: Data looks wrong
  3. Unknown Vendor: No match in system
  4. High Value: Needs approval before entry
  5. 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:

MetricBeforeAfter
Processing time per invoice5-10 min30 sec
Error rate3-5%<1% (with AI extraction)
Cost per invoice$12-15$1-2
Monthly hours saved15-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:

  1. Approval Workflows: Route high-value invoices for approval
  2. Early Payment Optimization: Flag invoices with early payment discounts
  3. Spend Analytics: Dashboard of spending by vendor/category
  4. Payment Automation: Trigger payments when due

Need help setting this up for your specific accounting software? Book a free consultation.