Every supplier lays out an invoice differently
The fields are always the same five. Where they sit on the page never is. Read the invoice properly, then let the model pull the fields out of the text.
You need the invoice number, the date, the net amount, the total and the VAT number. Five fields, the same five every time, and forty suppliers who each put them somewhere else.
One prints the number in the header, one in a boxed panel on the right, one only in the payment stub at the bottom. One writes Invoice no., one writes Doc., one writes nothing at all and just prints the number under the logo. Net and total change places depending on whether the tax lines are itemised. A third of them arrive as scans, because someone printed the PDF, signed it and put it back on the glass.
The rule you wrote against the first ten works on the first ten. On the eleventh it does not come back empty, which you would notice. It comes back with a number that is on the page and is not the total.
A rule that looks for the label Invoice No. and takes what follows it is fitted to one layout. The next supplier writes the label on its own line with the value underneath, and the rule takes the word that follows the label, which is now the date. You end up with one rule per supplier, and a supplier changes its billing template without telling you.
The deeper problem is that the label is not next to its value in the text, it is next to it on the page. Extraction returns characters in the order they were written into the file, so a label in a top-right panel and the value printed beneath it can be hundreds of characters apart in the string, or in the opposite order. Rules that count on proximity are counting on an accident of how the generator emitted the file, and that accident is different for every supplier.
And when the invoice is a scan there is no text to run a rule over at all. The pattern matches nothing, the field comes back empty, and an empty field is indistinguishable from a field that was genuinely not on the invoice.
Two steps, and the second one is not a tool call. read returns the text with the structure intact. Pulling five fields out of readable text is what a model is already good at, so that part is yours.
upload(filename="invoice-0141.pdf", content_type="application/pdf")
-> key, upload_url
curl -T invoice-0141.pdf "<upload_url>"
read(key="<key>", mode="exact")
-> handle, engine, used_fallback, cost_eur, structure, text
# an invoice is a page or two, so `text` comes back whole
# and the model reads the five fields out of it
result(handle="<handle>", find="VAT")
-> excerpts, each with the page it falls onexact costs 0.016 EUR per page and goes straight to an engine, so a two page invoice is 0.032 EUR. It is the mode to use when the layout carries the meaning, which on an invoice it always does.
balanced costs 0.007 EUR per page and tries the local text layer first. That attempt is free, so on a run of suppliers where some send machine-generated PDFs and some send scans, you pay only for the scans.
The same invoice read twice is charged once. The file is recognised by its content and not by its name, so the copy your accounts inbox renamed is still the same document and does not cost a second time. A read that fails is not charged at all.
- This returns text, not fields. There is no invoice schema, no field names, no JSON of the document: the five values are extracted by the model from the text, and that is the step you check.
- If a single page of the document fails the quality check, the whole document goes to an OCR engine. A check on the document average would hand back the blank pages at full price.
- Email files (EML, MSG) and ebooks (EPUB) are read by no engine. Extract the text or convert to PDF.
- An email with the invoice attached is not a document. Pull the attachment out first, then upload the attachment.
Without markup
This page in markdown: /problems/extract-fields-from-an-invoice.md. All 20 of them in one file: /llms-full.txt.