Five years ago, extracting structured data from invoices meant a deep learning pipeline: an OCR stage, a layout-aware model (LayoutLM, Donut) fine-tuned on thousands of hand-labeled documents, and a maintenance burden every time a vendor changed their template. In 2026 the entire pipeline collapses into one vision-LLM API call: image in, schema-validated JSON out — zero training data, zero hosted models, and new layouts handled zero-shot. This guide shows the full working pattern, what it costs per invoice, and how to keep accuracy production-grade.
Why LLMs replaced the invoice deep-learning stack
| Fine-tuned layout model (2021) | Vision LLM (2026) | |
|---|---|---|
| Training data | 3,000–50,000 labeled invoices | None (zero-shot) — a few examples help edge cases |
| New vendor layout | Often needs re-labeling + retraining | Handled zero-shot |
| Infrastructure | GPU serving + OCR stage | One HTTPS call |
| Output format | Token tags → custom decoding | Schema-constrained JSON |
| Cost per invoice | Engineering time dominates | $0.00042–$0.00426 |
A fine-tuned extractor can still win on a single frozen high-volume layout, but for the long tail of real-world invoices — different vendors, languages, scan quality — the zero-shot vision LLM is more accurate in practice and radically cheaper to own. More patterns for this class of workload live in the data-extraction use case.
The complete pattern: image → schema-validated JSON
Everything below runs against Kunavo's OpenAI-compatible endpoint — point base_url at https://api.kunavo.com/v1 and the same code can hit Claude, Gemini or GPT by changing one string:
from openai import OpenAI
import base64, json
client = OpenAI(
base_url="https://api.kunavo.com/v1",
api_key="sk-kunavo-...",
)
SCHEMA = {
"name": "invoice",
"schema": {
"type": "object",
"properties": {
"vendor_name": {"type": "string"},
"vendor_tax_id": {"type": ["string", "null"]},
"invoice_number": {"type": "string"},
"invoice_date": {"type": "string", "description": "ISO 8601"},
"due_date": {"type": ["string", "null"]},
"currency": {"type": "string", "description": "ISO 4217"},
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {"type": "string"},
"quantity": {"type": "number"},
"unit_price": {"type": "number"},
"amount": {"type": "number"},
},
"required": ["description", "amount"],
},
},
"subtotal": {"type": ["number", "null"]},
"tax": {"type": ["number", "null"]},
"total": {"type": "number"},
},
"required": ["vendor_name", "invoice_number", "invoice_date",
"currency", "line_items", "total"],
},
}
def extract(path: str) -> dict:
image_b64 = base64.b64encode(open(path, "rb").read()).decode()
resp = client.chat.completions.create(
model="claude-haiku-4-5", # or gemini-2-5-flash for the cheapest run
response_format={"type": "json_schema", "json_schema": SCHEMA},
messages=[
{"role": "system", "content":
"Extract the invoice into the schema. Copy values exactly as "
"printed; use null when a field is absent. Never invent data."},
{"role": "user", "content": [
{"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{image_b64}"}},
]},
],
)
return json.loads(resp.choices[0].message.content)
inv = extract("invoice_0231.png")
# Cheap arithmetic guardrail: reject when the line items don't add up.
delta = abs(sum(li["amount"] for li in inv["line_items"])
+ (inv.get("tax") or 0) - inv["total"])
if delta > 0.02:
raise ValueError(f"line items disagree with total by {delta:.2f}")
print(inv["vendor_name"], inv["total"], inv["currency"])Three details do most of the work: the JSON schema in response_format (the model cannot emit anything else), the system-prompt rule “copy values exactly; null when absent; never invent” (kills hallucinated tax IDs), and the arithmetic guardrail — line items + tax must equal the total, or the document goes to a stronger model or a human. See the chat completions reference for the structured-output options.
Cost per invoice, by model
A one-page invoice ≈ 1,800 input tokens (sent as an image) + ~350 output tokens of JSON:
| Model | Per invoice | Per 10,000 invoices | Use for |
|---|---|---|---|
gemini-2-5-flash | $0.00042 | $4.20 | Cheapest bulk runs |
claude-haiku-4-5 | $0.00142 | $14.20 | Default: clean printed invoices |
claude-sonnet-4-6 | $0.00426 | $42.60 | Escalation tier: scans, handwriting, failures |
Rates are live from the pricing page (about 60% under the providers' list prices), and failed requests are not billed. With two-tier routing — everything through Haiku, arithmetic-check failures escalated to Sonnet — 10,000 invoices/month typically lands under $17.04.
Accuracy: the checklist that gets you to production
- Schema first. Mark truly-required fields as
required, allownulleverywhere else — forcing a value forces a hallucination. - Send the image, not OCR text. Layout carries meaning (columns, totals boxes); vision models read it directly. Only fall back to text for born-digital PDFs where you can extract a clean text layer.
- Validate arithmetic in code. Line items + tax = total catches most extraction errors for free.
- Escalate, don't retry blindly. Failures go to Sonnet with the validation error included in the prompt; persistent failures go to a human queue.
- Few-shot the true edge cases. Two or three examples of your worst layouts (credit notes, multi-currency) in the system prompt beat any amount of instruction tuning.
Beyond invoices
The same schema-constrained pattern covers receipts, purchase orders, delivery notes, customs forms and bank statements — swap the schema, keep the guardrails. Browse the broader structured data extraction use case for RAG-adjacent patterns, or the cost optimization guide for routing strategies once volume grows. One Kunavo key covers every model tier used here — sign up free and create a key to run the snippet as-is.
FAQ
Can I extract data from PDF invoices, not just images?
Yes — render each PDF page to PNG (e.g. with pdf2image) and send it as above. For born-digital PDFs you can also extract the text layer and send it as plain text, which is cheaper; keep the image path for scans.
How accurate is LLM invoice extraction?
On clean printed invoices, schema-constrained vision extraction with the arithmetic guardrail routinely clears 98%+ field-level accuracy — and unlike a fine-tuned model, accuracy holds on layouts it has never seen. Measure on your own documents: 100 labeled invoices make a solid eval set.
Is my invoice data used for training?
No — requests through Kunavo are passed to the model providers under API-usage terms (not consumer-app terms) and are not used to train models. See the billing & data docs for retention details.