Jun 23, 2026
Invoice Data Extraction: When an LLM Beats OCR Templates and When It Does Not
Inference Research
Pick the Tool That Matches the Invoice, Not the Hype
Invoice data extraction is the process of turning an invoice into structured fields your software can use: vendor, invoice number, dates, line items, tax, and totals. The catch is that "an invoice" can be a scanned image, a digital PDF, an HTML page on a vendor portal, or text that already came out of an OCR step, and each of those formats wants a different tool. Choosing the wrong one for your format is the most common and most expensive mistake in this space.
This article is deliberately a boundary-setting guide. It draws a clear line between three things that get lumped together under "invoice data extraction": OCR, document intelligence, and schema-guided LLM extraction. You will see exactly when a cloud document tool from Azure, AWS, or Google is the right first step, and when a schema-guided large language model beats template-based OCR on invoice-like HTML or on text you already have. There is a worked example, an invoice schema, a validation step, and a cost workflow, plus an honest section on when this approach is the wrong layer entirely.
Read time: 10 minutes
OCR, Document Intelligence, and Schema-Guided LLM Extraction Are Not the Same Thing
Three layers get conflated constantly, and keeping them straight is most of the battle. Here is how they differ.
| Layer | Input | What you get | When to use |
|---|---|---|---|
| OCR | Scan, photo, PDF | Characters + positions | Need raw text from pixels |
| Document intelligence | Scan, photo, PDF | Named invoice fields + confidence | Scanned or image invoices |
| Schema-guided LLM | HTML or text | JSON matching your schema | Invoice HTML or post-OCR text |
OCR, or optical character recognition, converts pixels into characters and word positions. It reads a scanned PDF or a photo and gives you back text and geometry, but it does not assign meaning. It will happily tell you that the string $1,240.00 appears near the bottom-right of the page; it will not tell you that this is the invoice total rather than a line-item subtotal.
Document intelligence, sometimes called intelligent document processing, sits a layer up. These are layout-aware models trained on documents that take an image or PDF and return named fields, and usually line items, end to end. Azure AI Document Intelligence ships a prebuilt invoice model that outputs structured JSON with a per-field confidence score for every value, which is what lets a downstream system auto-process the confident extractions and route the rest to a human. AWS Textract's AnalyzeExpense returns typed ExpenseDocument objects with summary fields and line-item groups straight from invoice and receipt images. Google's Document AI offers an Invoice Parser processor that does the same layout-aware field extraction.
Schema-guided LLM extraction is the third layer, and it works differently. A model in this category takes input that is already text, such as HTML or OCR output, plus a schema you define, and returns JSON that conforms to that schema. Schematron is this kind of model: it extracts typed JSON from messy HTML against your Pydantic, Zod, or JSON Schema definition. It is not an OCR engine and it does not read images. If you want to understand the underlying mechanics, the schema-guided extraction docs cover the model and its constraints in detail.
The decision rule falls out of the table. If your invoice is a scanned image or a PDF you cannot reliably pull clean text from, start with document intelligence. If the invoice data is already HTML, an emailed receipt, or text from an OCR step, that is where schema-guided extraction earns its place.
Figure 1: Invoice Extraction Pipeline by Format
When Azure, AWS, or Google Document Tools Are the Better First Step
Be honest with yourself about the input. If your invoices arrive as scans from a multifunction printer, photos snapped on a phone, or PDFs where the text layer is unreliable, a document-intelligence tool is the better first step, full stop. This is the category most people mean when they search for invoice data extraction software, and for raw documents they are right to. These services do the pixels-to-fields path end to end, they come with prebuilt invoice schemas that already include line items, and they return per-field confidence scores. Textract's AnalyzeExpense and Google's Invoice Parser give you the same kind of typed, layout-aware output without you training anything.
The confidence scores matter more than they first appear. A scanned-invoice workflow lives or dies on knowing which fields to trust, and document-intelligence tools hand you that signal per field, which makes the auto-process-versus-review decision mechanical. Independent 2026 benchmarks put field accuracy on clean documents in the mid-90s for these tools, with Google Document AI around 95.8% and AWS Textract around 94.2%, and both degrade on faxes, photos, and old photocopies; vendors advertise 95% or better out of the box. Treat those as vendor and third-party figures rather than numbers we measured. The point still stands: extracting fields from raw documents is a solved problem, and you should not rebuild it.
Here is the line to keep in mind: a schema-guided LLM like Schematron will not read a scanned image. Asking one to do OCR is asking the wrong layer to do a job it was never built for. The interesting question is what happens after the document tool has produced text and fields, because that output is rarely shaped exactly like your system's data model. That is the bridge to the next section.
When a Schema-Guided LLM Is the Right Fit for Invoices
There are two situations where you should extract structured data from invoices with a schema-guided model rather than a document tool, and both share a property: the data is already text.
Invoice Data Already in HTML, Email, or a Vendor Portal
A surprising share of invoices never become images. They live as HTML on a supplier's billing portal, or as an order confirmation or receipt in an email body. Running OCR on that content would be a step backward; you would be rasterizing structured text only to recognize it again. A schema-guided model reads the HTML directly and turns it into your schema. Because the model is long-context, a sprawling vendor-portal page with menus, banners, and a buried invoice table is still well within range. This is the same problem space as turning any web page into typed records, which the HTML-to-JSON extraction guide covers for the general case.
Normalizing Invoice Data After OCR
The second fit is downstream of OCR or document intelligence. Suppose Textract or Document AI has already extracted fields, but in their structure, not yours, and you process invoices from forty vendors with forty slightly different layouts. Instead of writing per-vendor mapping code, you hand the extracted text or a flattened version of those fields to a schema-guided model with one invoice schema, and it normalizes everything into a single typed record. One schema replaces a pile of per-template rules.
What makes this work is that the schema is the interface, not a prompt. Schematron does not take system or user instructions; every piece of guidance you would normally put in a prompt goes into the schema as field descriptions instead. That is why the output stays repeatable: there is no prose to drift, only a contract. Throughout, keep acquisition and extraction as separate steps. Fetching the page or running OCR is one job; turning that text into a validated record is another.
A Worked Example: From Invoice HTML to Validated JSON
Let's make this concrete with an invoice that arrived as HTML on a vendor portal. The invoice extraction pipeline has five steps: define the schema, clean the HTML, call the model, look at the output, and validate.
Here is a trimmed, messy version of the kind of HTML you would get from a portal:
<div id="invoice" class="doc">
<header><img src="/logo.png" alt=""><h1>NorthByte Supply Co.</h1></header>
<p>Invoice <b>#INV-20418</b></p>
<span class="meta">Issued: 2026-05-28</span> <span class="meta">Due: 2026-06-27</span>
<table class="lines">
<tr><th>Item</th><th>Qty</th><th>Unit</th><th>Amount</th></tr>
<tr><td>USB-C cable, 2m</td><td>10</td><td>$6.50</td><td>$65.00</td></tr>
<tr><td>Mechanical keyboard</td><td>2</td><td>$89.00</td><td>$178.00</td></tr>
</table>
<div class="totals">
<p>Subtotal: <b>$243.00</b></p>
<p>Tax (8%): <b>$19.44</b></p>
<p class="grand">Total Due: <b>$262.44 USD</b></p>
</div>
<footer>Questions? billing@northbyte.example</footer>
</div>The Invoice Schema
Start by describing the record you want. Because the schema carries all of the extraction guidance, the field descriptions do real work here, not decoration. A Pydantic model keeps the schema and the validator in one place:
from pydantic import BaseModel, Field
class LineItem(BaseModel):
description: str = Field(..., description="Item or service description as shown on the line.")
quantity: float = Field(..., description="Quantity billed for this line.")
unit_price: float = Field(..., description="Price per unit before tax.")
amount: float = Field(..., description="Line total (quantity * unit_price).")
class Invoice(BaseModel):
invoice_number: str = Field(..., description="Invoice identifier, e.g. INV-20418.")
vendor_name: str = Field(..., description="Name of the vendor or supplier issuing the invoice.")
invoice_date: str = Field(..., description="Issue date in ISO 8601 (YYYY-MM-DD).")
due_date: str | None = Field(None, description="Payment due date in ISO 8601, if present.")
currency: str = Field(..., description="ISO 4217 currency code, e.g. USD.")
line_items: list[LineItem] = Field(default_factory=list, description="All billed line items.")
subtotal: float = Field(..., description="Sum of line item amounts before tax.")
tax: float = Field(..., description="Total tax applied to the invoice.")
total: float = Field(..., description="Amount due (subtotal + tax).")Cleaning the HTML
Schematron was trained on HTML that had been pre-cleaned with lxml to strip scripts, styles, and inline JavaScript, so aligning your preprocessing with that improves results and trims input tokens. Other tools such as Readability, Trafilatura, or BeautifulSoup are fine; lxml simply matches the training.
Calling Schematron
The call uses the OpenAI-compatible API, so if you have used the OpenAI SDK before this will look familiar. Point the client at the inference.net base URL, keep temperature at zero, and pass your model as the response format. The combination of the schema and strict JSON mode is what guarantees the output parses; you can read more about that behavior in the structured outputs documentation, and the API quickstart covers the base-URL and key setup.
import os
import lxml.html as LH
from lxml.html.clean import Cleaner
from openai import OpenAI
from invoice_schema import Invoice
HTML_CLEANER = Cleaner(
scripts=True,
javascript=True,
style=True,
inline_style=True,
safe_attrs_only=False,
)
def strip_noise(html: str) -> str:
"""Remove scripts, styles, and JavaScript from HTML using lxml."""
if not html or not html.strip():
return ""
try:
doc = LH.fromstring(html)
cleaned = HTML_CLEANER.clean_html(doc)
return LH.tostring(cleaned, encoding="unicode")
except Exception:
return ""
client = OpenAI(
base_url="https://api.inference.net/v1",
api_key=os.environ.get("INFERENCE_API_KEY"),
)
invoice_html = strip_noise(open("invoice.html").read())
resp = client.beta.chat.completions.parse(
model="inference-net/schematron-v2-small",
messages=[
{"role": "user", "content": invoice_html},
],
response_format=Invoice,
temperature=0,
)
print(resp.choices[0].message.parsed.model_dump_json(indent=2))Representative JSON Output
For the HTML above, the model returns JSON that conforms to the schema. A representative result:
{
"invoice_number": "INV-20418",
"vendor_name": "NorthByte Supply Co.",
"invoice_date": "2026-05-28",
"due_date": "2026-06-27",
"currency": "USD",
"line_items": [
{
"description": "USB-C cable, 2m",
"quantity": 10,
"unit_price": 6.5,
"amount": 65.0
},
{
"description": "Mechanical keyboard",
"quantity": 2,
"unit_price": 89.0,
"amount": 178.0
}
],
"subtotal": 243.0,
"tax": 19.44,
"total": 262.44
}Validation and Exception Handling
Schematron is documented to return schema-valid JSON, but you should still validate on ingest, because schema-valid is not the same as business-valid. Parsing with Pydantic catches structural problems for free; the more useful checks are arithmetic and completeness. Do the line items sum to the subtotal? Does subtotal plus tax equal the total? Is the currency a real ISO code, and do the dates parse? When any of those fail, the invoice goes to an exception queue keyed by invoice number instead of into your ledger.
from datetime import date
from invoice_schema import Invoice
TOLERANCE = 0.01 # currency rounding
def route_to_review(invoice_number: str, reason: str) -> None:
"""Send a low-confidence invoice to the exception queue."""
print(f"REVIEW {invoice_number}: {reason}")
def validate(invoice: Invoice) -> bool:
"""Return True if the invoice passes business rules, else queue it for review."""
line_sum = round(sum(item.amount for item in invoice.line_items), 2)
if abs(line_sum - invoice.subtotal) > TOLERANCE:
route_to_review(invoice.invoice_number, f"line items {line_sum} != subtotal {invoice.subtotal}")
return False
if abs((invoice.subtotal + invoice.tax) - invoice.total) > TOLERANCE:
route_to_review(invoice.invoice_number, "subtotal + tax != total")
return False
if len(invoice.currency) != 3 or not invoice.currency.isalpha():
route_to_review(invoice.invoice_number, f"invalid currency {invoice.currency!r}")
return False
try:
date.fromisoformat(invoice.invoice_date)
except ValueError:
route_to_review(invoice.invoice_number, f"unparseable invoice_date {invoice.invoice_date!r}")
return False
return True
invoice = Invoice.model_validate_json(open("invoice_output.json").read())
if validate(invoice):
print(f"AUTO-POST {invoice.invoice_number}: total {invoice.total} {invoice.currency}")That validation step is also where this pattern slots into a larger system. Extraction is one stage in an architecture that includes acquisition, schema design, validation gates, and monitoring, which is the subject of building automated extraction as an architecture.
For a high-volume normalization job where cost and latency dominate, Schematron V2 Turbo is the natural starting point.
Try Schematron V2 Turbo
Use Schematron V2 Turbo for high-throughput HTML-to-JSON extraction when cost and latency matter.
A Cost and Confidence Workflow
Two levers control your cost, and one rule controls your risk.
Choosing Between Schematron V2 Small and Turbo
The two models trade quality against throughput and price.
| Model | Input $/M | Output $/M | Best for |
|---|---|---|---|
| V2 Small | $0.05 | $0.25 | Complex schemas, long pages |
| V2 Turbo | $0.03 | $0.15 | High-volume normalization |
Pricing per million tokens, from the model pages (current).
Turbo runs at roughly 4.14 requests per second on a single H100 against Small's 2.47, on a ten-thousand-token input, which is why it is the default for high-volume invoice normalization. Reach for Small when the schema is complex, the page is very long, or the invoices are genuinely hard, where its higher extraction quality earns the extra cost. The other lever is the input itself: pre-cleaning HTML with lxml before extraction cuts the token count you pay for on every call.

On the quality side, the V2 models hold up well. On an LLM-as-a-judge extraction benchmark scored from one to five, V2 Small lands at 4.060 and V2 Turbo at 4.039, essentially matching the previous-generation 8B model while running faster and cheaper. The full Schematron V2 quality metrics include the throughput and factual-accuracy numbers behind those scores.

Routing on Confidence
Here is the one place where document intelligence has a structural edge worth naming. Schematron returns JSON that conforms to your schema, but it does not hand you a calibrated per-field confidence the way Azure's invoice model does. So you derive confidence from validation instead: an invoice where the line items reconcile, every required field is present, and the totals add up is high-confidence and can post automatically; anything that fails a check goes to review. When you are extracting from OCR text, you can also carry the OCR engine's own confidence forward and fold it into the same gate. The practical upshot is that the confidence story is one more reason to let document intelligence handle raw scans first.
When Schematron Is Not the Right Layer
This is the section that matters most, because using the wrong layer wastes time and money. Schematron is not the right tool when:
- The input is a raw scan, photo, or untextual PDF. It is not an OCR engine and will not read an image. Run OCR or a document-intelligence tool first.
- You need crawling, fetching, or browser automation. Acquiring the page or document is a separate layer; get the text first, then extract.
- You need native, calibrated per-field confidence with no validation logic of your own. Document-intelligence tools provide that directly out of the box.
- You want to steer extraction with written instructions. There is no prompt channel; if a behavior is not expressible in the schema, the model cannot follow it.
- A field requires synthesis the schema does not describe. Ambiguous fields that need summarizing or inference must be spelled out in the schema, and some genuinely will not be.
None of this is a knock on the model. It is a map. The strongest invoice pipelines usually run both layers: document intelligence turns a scan into text and fields, then a schema-guided model normalizes that into one clean record across every vendor.
Putting the Pipeline Together
Invoice data extraction stops being guesswork once you let the format pick the tool. The whole approach reduces to a short, repeatable sequence:
- Acquire the invoice, fetching the HTML or running OCR or document intelligence on a scan.
- Clean the text, using
lxmlfor HTML to strip noise. - Define one schema for the record you want.
- Extract with a schema-guided model into conforming JSON.
- Validate and route, posting confident records and queueing the rest.
When you are running this across thousands of invoices, the Batch API takes up to 50,000 jobs at once with optional webhook updates, which is the path for recurring vendor-portal runs and large back-catalog migrations. If your volume is high enough to need guaranteed throughput, that is the point to talk about dedicated capacity.
To extract typed JSON from your own invoice HTML or post-OCR text, the fastest start is sending a sample page and a schema to the model and seeing what comes back.
Extract typed JSON from messy HTML
Send HTML and a Pydantic, Zod, or JSON Schema definition to Schematron and get structured JSON back without writing selectors or prompts.
Related Reading
- HTML-to-JSON Extraction API — the general schema-driven extraction case for any web page, behind the invoice-HTML scenario.
- Data Extraction Tools for Web Data — how to segment extraction tools by source, including PDFs and documents, and pick the right layer.
- Best LLM for Data Extraction — Schematron V2 quality metrics and a method for evaluating models on your own pages.
- Financial Data Extraction Software for Web Pages & Filings — the same extraction tradeoffs applied to financial pages, filings, and statements.
Meet with our research team
Schedule a call with our research team to learn more about how Specialized Language Models can cut costs and improve performance.