Most small businesses receive supplier invoices as PDF attachments and re-type the header data into their accounting system by hand. A large language model can read those PDFs and return the fields as structured data. The hard part is not the extraction itself but doing it reliably, with checks and a human decision before anything is booked. This article is for business owners and IT staff who want a working pattern they can test in an afternoon.
What to automate, and what not to
Invoice intake is a good first LLM project because the input is bounded (a document), the output has a fixed shape (a handful of fields) and every result can be checked with simple arithmetic. The model should only read the document. Decisions such as posting, paying or approving stay with deterministic rules and a person.
A sensible split:
- LLM: read the PDF and return supplier, invoice number, date, currency and totals.
- Your code: validate the values, detect duplicates, apply approval thresholds.
- A person: approves every draft, at least during the first weeks and for anything unusual.
Step 1: Get structured output instead of free text
If you ask a model for JSON in the prompt only, you will eventually get a stray sentence or a missing field. The Claude API offers structured outputs: you pass a JSON schema in output_config.format and the response text is valid JSON matching it. The feature is generally available and needs no beta header (see the Anthropic documentation linked below). Other providers offer similar features; the pattern stays the same.
Points from the documentation that matter in practice:
- Every object in the schema must set
additionalPropertiestofalse. - Numeric constraints such as
minimumormaximumand string constraints such asmaxLengthare not supported in raw schemas and lead to an error. Do these checks in your own code. - If
stop_reasonisrefusalormax_tokens, the output may not match your schema. Treat that as a failure and route the document to manual review.
Why amounts are strings
The schema below returns amounts as strings. You then convert them to Decimal in Python. This avoids floating-point rounding and lets you reject anything that is not a clean number.
Step 2: Extract the invoice data
The following sketch sends a PDF as a base64 document block (according to the documentation, a request may be up to 32 MB and contain up to 600 PDF pages, or 100 pages when the request's context window is under 1M tokens; for large files you can upload them via the Files API to keep the request small, and documents above the page limit need splitting). Install the SDK with pip install anthropic and set ANTHROPIC_API_KEY in the environment of the service account that runs the script, not in the code.
import base64
import json
from decimal import Decimal, InvalidOperation
import anthropic
MODEL = "claude-sonnet-5-5" # pin an explicit model and re-test before changing it
INVOICE_SCHEMA = {
"type": "object",
"properties": {
"supplier_name": {"type": "string"},
"invoice_number": {"type": "string"},
"invoice_date": {"type": "string", "format": "date"},
"currency": {"type": "string"},
"net_total": {"type": "string"},
"vat_total": {"type": "string"},
"gross_total": {"type": "string"},
},
"required": [
"supplier_name", "invoice_number", "invoice_date",
"currency", "net_total", "vat_total", "gross_total",
],
"additionalProperties": False,
}
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the environment
def extract(pdf_path):
with open(pdf_path, "rb") as f:
pdf_b64 = base64.standard_b64encode(f.read()).decode("utf-8")
response = client.messages.create(
model=MODEL,
max_tokens=16000, # thinking counts toward max_tokens; leave room for it
messages=[{
"role": "user",
"content": [
{"type": "document",
"source": {"type": "base64",
"media_type": "application/pdf",
"data": pdf_b64}},
{"type": "text",
"text": "Extract the invoice header data. Amounts as plain "
"decimal strings with a dot, e.g. 1234.50. Copy "
"values exactly as printed; do not calculate or guess."},
],
}],
output_config={"format": {"type": "json_schema", "schema": INVOICE_SCHEMA}},
)
if response.stop_reason in ("refusal", "max_tokens"):
return None # output may not match the schema: send to manual review
text = next(b.text for b in response.content if b.type == "text")
return json.loads(text)
Note: On current models such as Claude Sonnet 5.5, adaptive thinking is on by default and thinking tokens count toward max_tokens. The documentation warns that a max_tokens value sized for a response without thinking is often too small; if you see stop_reason max_tokens, raise the limit or lower the effort level.
Note: This is a minimal sketch. Add error handling for network errors and rate limits (the SDK retries some errors automatically), and log only what you need, not the full document content.
Step 3: Validate with plain code
The model can misread a digit. The cheapest protection is a set of checks that do not depend on the model: net plus VAT must equal gross, the currency must be one you expect, the invoice number must not already exist for that supplier, and the supplier should be known. Anything that fails goes to a review queue.
def needs_review(data):
"""Return a list of reasons; an empty list means the checks passed."""
if data is None:
return ["extraction failed"]
reasons = []
try:
net = Decimal(data["net_total"])
vat = Decimal(data["vat_total"])
gross = Decimal(data["gross_total"])
except InvalidOperation:
return ["amount is not a valid number"]
if net + vat != gross:
reasons.append("net + VAT does not equal gross")
if data["currency"] != "EUR":
reasons.append("unexpected currency")
# Add your own rules, e.g. known supplier list, duplicate invoice number,
# amount above your approval threshold.
return reasons
result = extract("incoming/<invoice-file>.pdf")
reasons = needs_review(result)
if reasons:
print("-> manual review queue:", reasons)
else:
print("-> prepare draft (not posted) vendor bill for approval:", result)
Keep a person in the loop
The script ends in a draft that someone approves; it never posts or pays on its own. This is good accounting practice, and it also keeps you clear of GDPR Article 22, which gives people the right not to be subject to decisions based solely on automated processing where these have legal or similarly significant effects. Reading header data from supplier invoices is rarely such a decision, but an automatic approval or payment workflow should be reviewed with that article in mind.
Step 4: Data protection and compliance
Invoices contain personal data: names of contact persons, sole traders as suppliers, bank details. Before you send documents to an external API:
- Processor agreement. If the provider processes personal data on your behalf, GDPR Article 28 requires a provider that offers sufficient guarantees for appropriate technical and organisational measures, and a binding contract or other legal act governing the processing (usually a data processing agreement). Check the provider's data processing terms and sub-processors, and where data is processed.
- Retention. Read the provider's data retention documentation and choose the setting that fits. Structured outputs are, for example, listed as eligible for zero data retention agreements in Anthropic's documentation, which is an arrangement you have to agree with the provider, not a default.
- Data minimisation. Send only what is needed. If you only need header data, do not send unrelated attachments or whole mailboxes.
- Risk assessment. Article 35 requires a data protection impact assessment where processing is likely to result in a high risk. A simple invoice workflow usually does not, but document why you came to that conclusion.
- Records and notices. Add the workflow to your record of processing activities and update your privacy notice if needed.
- EU AI Act. Using a general-purpose model for invoice reading is generally not a high-risk use case. Article 4 (AI literacy), as amended by the Digital Omnibus on AI, requires providers and deployers of AI systems to take measures to support the AI literacy of their staff; it does not require a specific level of AI literacy for any individual. In practice, staff who work with the workflow should understand its limits. The AI Act has been amended; check the current text and your national guidance before you rely on a deadline.
Note: This is a technical overview, not legal advice. Have your data protection officer or a lawyer review the setup.
Step 5: Test before you trust it
- Collect 30 to 50 real invoices from different suppliers, including poor scans and non-standard layouts.
- Write the correct values down once and compare them with the output.
- Count the errors by field to see where the model needs more review.
- Pin the model name, and re-run the test set whenever you change the model or the prompt.
- Run the workflow in shadow mode for a few weeks: the script produces results, staff keep booking by hand and compare.
Where this fits into Odoo or other systems
The extracted fields can be turned into a draft vendor bill through the accounting system's API or an import. Whichever system you use, create drafts only, attach the original PDF and store the reasons from the validation step, so the reviewer can see why a document was flagged.
Checklist
- Model only reads documents; rules and people decide.
- JSON schema with
additionalProperties: false; amounts as strings, converted toDecimal. - Treat
refusalandmax_tokensas failures. - Independent checks: totals, currency, duplicates, known supplier.
- Drafts only, with human approval.
- Data processing agreement, retention settings and record of processing in place.
- Model pinned; test set re-run after every change.
- API key stored outside the code, with minimal permissions.
Our take
LLM-based document reading is useful today for repetitive intake tasks, as long as it is treated as an assistant with checks around it rather than an autonomous bookkeeper. We recommend starting with one document type, measuring the error rate on your own data and keeping approval with your team.
Sources
- Anthropic documentation: Structured outputs
- Anthropic documentation: PDF support
- Anthropic documentation: Steering thinking (cost control and max_tokens)
- Anthropic documentation: API and data retention
- Regulation (EU) 2016/679 (GDPR), Articles 22, 28 and 35
- Regulation (EU) 2024/1689 (AI Act)
- European Commission AI Act Service Desk: Article 4 (current, amended text)
- Python documentation: decimal