Skip to content
Invoice and Receipt Extractor

Module 07 · business · intermediate

Lesson 07 · Invoice and Receipt Extractor

Goal

Process files with AI: accept a PDF upload, read its text, and turn messy receipt text into structured data in a spreadsheet. "Document → structured data" is one of the most valuable automations you can sell.

Prerequisites

  • Modules 03 (Sheets) and 06 (OpenAI credential, Information Extractor)

Concepts

  • Binary data: files travel next to an item's JSON in $binary. A multipart upload field named file becomes $binary.file, with mimeType, fileName and the data.
  • Extract From File: reads text out of PDFs (plus CSV, XLSX, HTML…) into $json.text. It works on PDFs with a text layer; scanned images need OCR or a vision model (see Exercise 2).
  • JSON-schema extraction: for nested data like line items, give the Information Extractor a JSON example instead of individual attributes.
  • Dry run: a query flag that skips side effects. Great for testing and for demos.
  • needs_review: when extraction can't find a key value, flag it instead of silently saving garbage.

Build it

Step 1: Upload and validate

  • Webhook Receipt API: POST, path receipt, respond using a Respond to Webhook node.
  • IF Has PDF?: {{ Object.values($binary ?? {}).some(f => f.mimeType === 'application/pdf') }} is true. False → Respond to Webhook (400, explain how to upload).

Step 2: Read the PDF

Extract From File Extract PDF Text: operation Extract From PDF, Input Binary Field {{ Object.keys($binary).find(k => $binary[k].mimeType === 'application/pdf') }} (works whatever the upload field is called), Options → Join Pages on, Max Pages 5.

Run it with the sample file and look at text:

curl -X POST http://localhost:5678/webhook-test/receipt -F "file=@assets/sample-receipt.pdf;type=application/pdf"

Step 3: Extract with AI

Information Extractor Extract Receipt Data:

  • Text: {{ $json.text.slice(0, 8000) }}
  • Schema Type: Generate From JSON Example, with an example containing vendor, invoice_number, date, currency, subtotal, tax, total, category and a line_items array of {description, quantity, amount}
  • System prompt: dates as YYYY-MM-DD, amounts as plain numbers, ISO currency codes, category from a fixed list, null when missing, never invent values
  • Model: OpenAI Chat Model → gpt-5-mini

Step 4: Clean

Edit Fields Clean Receipt: numbers via Number(... ?? 0), currency uppercased, items_count = {{ ($json.output.line_items ?? []).length }}, needs_review = {{ !$json.output.total || !$json.output.date || !$json.output.vendor }}, processed_at = now.

Step 5: Save (unless dry run)

IF Dry Run?: {{ $('Receipt API').item.json.query.dry_run === 'true' }}.

  • false → Google Sheets Save to Expenses (Append Row, sheet Expenses with headers from assets/expenses-template.csv) → Respond
  • true → Respond directly

Respond: {{ { saved: $("Receipt API").item.json.query.dry_run !== "true", ...$("Clean Receipt").item.json } }}.

Test it

./test/test.sh                       # upload validation
WITH_CREDS=1 ./test/test.sh          # + extraction checks on the sample receipt (dry run)
WITH_CREDS=1 SAVE=1 ./test/test.sh   # + save to the sheet

The sample receipt's known values: Brewlab Coffee Co., INV-20481, 2026-09-28, USD, subtotal 27.00, tax 2.23, total 29.23, 3 items.

Common problems

  • 400 even though you sent a PDF: curl guessed the wrong type. Add ;type=application/pdf after the filename.
  • Empty text: the PDF is a scanned image (no text layer). Use Exercise 2's vision approach.
  • Totals like "$29.23" or 29,23: tighten the system prompt; Number() turns non-numeric strings into NaN, which needs_review should catch (Exercise 1).
  • AI invents an invoice number: keep "never invent values; use null" in the prompt and check needs_review.