DEV Community

Mohamed Zaghloul
Mohamed Zaghloul Subscriber

Posted on

Extracting invoices from Gmail with n8n and Gemini, then checking the IBAN before anyone pays it

Most invoice automations stop at extraction: an LLM pulls the vendor, total and bank details out of a PDF, and the numbers go straight into a spreadsheet or ledger. The risky part is that an LLM can read a perfectly formatted IBAN with one wrong digit, and nothing downstream notices.

This post walks through a free n8n template that handles the extraction, and a small API I built that handles the checksum part. They're separate pieces: the template doesn't call the API out of the box. I'll show how to add it.

Part 1: the free n8n template

Nodes, in order:

  1. Gmail Trigger: polls for unread mail labelled invoices with a PDF attached.
  2. Split PDF Attachments (Code node): one item per PDF, so an email with three invoices produces three rows.
  3. Extract From File: reads the PDF's text layer. Scanned image-only PDFs need OCR first.
  4. Gemini + Structured Output Parser: returns vendor, invoice number, PO number, dates, currency, subtotal, tax, total, line items and a confidence score as strict JSON. Temperature 0, retries on rate limits.
  5. Normalize Invoice (Code node): cleans numbers and dates, checks that subtotal + tax = total, and marks low-confidence rows needs_review.
  6. Google Sheets: appends the row. Then Gmail marks the email read so it's never processed twice.

Setup takes about 10 minutes: a Gmail label and filter, a sheet with an Invoices tab, and three credentials (Gmail, Sheets, a free Gemini key from Google AI Studio).

Two things that made it more reliable:

  • Strict schema over free text. The Structured Output Parser rejects anything that isn't the expected JSON, so a malformed response fails loudly instead of writing garbage.
  • Check the math in code, not in the prompt. Asking Gemini "does this add up?" is slower and less reliable than one line of arithmetic in the Normalize node.

Part 2: validating payment IDs with an API

Extraction confidence doesn't tell you whether an IBAN is real. The ISO 13616 mod-97 checksum does. I put that, plus a few related checks, in a Cloudflare Worker:

  • IBAN: country, length, mod-97 checksum, SEPA membership
  • BIC: format and country
  • EU VAT: format, plus an optional live lookup against the EU's VIES service
  • UK VAT: format and checksum
  • US EIN: format and IRS prefix (EINs have no check digit, so that's as far as honest validation goes)

Up to 50 identifiers per batch request. Here's a real response from the keyless demo endpoint:

curl "https://invoice-validator.mohamed-bahaa53000.workers.dev/demo/v1/iban?value=DE89370400440532013000"
Enter fullscreen mode Exit fullscreen mode
{
  "iban": "DE89370400440532013000",
  "country": "DE",
  "check_digits": "89",
  "bban": "370400440532013000",
  "valid": true,
  "formatted": "DE89 3704 0044 0532 0130 00",
  "sepa": true,
  "errors": []
}
Enter fullscreen mode Exit fullscreen mode

The demo allows 10 calls per IP per day without a key.

Why a Worker rather than an n8n Code node? Mostly reuse: the same check is useful from Make, Zapier or a backend, and it's stateless, so it runs at the edge in milliseconds. It also isolates the slow part. VIES throttles by member state, so when it's unavailable the API returns checked: false with a reason instead of a false "invalid".

Part 3: wiring them together yourself

After Normalize Invoice, add an HTTP Request node:

  • Method: POST, to the /v1/batch endpoint from the RapidAPI listing
  • Headers: X-RapidAPI-Key and X-RapidAPI-Host
  • Body:
{
  "items": [
    { "id": "INV-1042-iban", "type": "iban", "value": "DE89370400440532013000" },
    { "id": "INV-1042-vat", "type": "eu_vat", "value": "DE123456789", "live": true }
  ]
}
Enter fullscreen mode Exit fullscreen mode

In n8n, map each value with an expression from the previous node, for example $json.invoice_number, $json.vendor_iban and $json.vendor_vat.

You'll need to add vendor_iban and vendor_vat to the Gemini schema first. Then treat valid: false as "needs review", and checked: false from VIES as "verify manually", not as a rejection. A person sees exactly why a row was flagged.

What I took away

  • Fail open to a human, never closed to the ledger. Anything that can time out should route to review.
  • Batch external checks. One call per identifier multiplies latency and quota.
  • Keep each piece honest about what it does. The template extracts; the API validates; you decide how strict to be.

Links

Feedback on either is welcome, especially edge cases where the extraction or the validation gets it wrong.

Top comments (0)