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 namedfilebecomes$binary.file, withmimeType,fileNameand 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, pathreceipt, 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,categoryand aline_itemsarray 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 fromassets/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/pdfafter the filename. - Empty
text: the PDF is a scanned image (no text layer). Use Exercise 2's vision approach. - Totals like
"$29.23"or29,23: tighten the system prompt;Number()turns non-numeric strings intoNaN, whichneeds_reviewshould catch (Exercise 1). - AI invents an invoice number: keep "never invent values; use null" in the prompt and check
needs_review.