Quotation OCR: Automate Quote-to-Invoice Extraction

Stack of supplier invoices and quotes next to a laptop running OCR extraction software on the screen

You're staring at a folder of 40 supplier quotes — some PDF, some scanned JPGs, one photo taken at an angle in a warehouse. Your ops person keys them into your accounting system by hand, and every fifth invoice gets flagged because a line item or tax code was mistyped. This post is the working playbook I use to kill that workflow with OCR plus an LLM, including the exact fields to extract, accuracy you can realistically hit, and where it still breaks.

What quotation OCR actually is (and what it isn't)

Quotation OCR is the process of turning a quote or invoice document — PDF, scan, or phone photo — into structured, machine-readable data (vendor, line items, totals, tax) that flows straight into your ERP, accounting tool, or approval workflow. Modern systems combine two layers: a layout-aware OCR engine (extracts text plus its position on the page) and an LLM or trained extraction model (maps that text to fields).

What it isn't: raw Tesseract on a JPG. That gets you a wall of text and no idea which number is the subtotal. It's also not "just paste it into ChatGPT" — that works for one document, not for 300/month with an audit trail.

The three architectures you'll see in 2026:

Approach How it works Best for
Rule/template OCR Fixed coordinates per vendor template High volume, few vendors, stable layouts
Layout OCR + LLM Text + bounding boxes → LLM extracts fields Mixed vendors, changing layouts, most SMBs
Vision LLM direct Feed the PDF/image to a multimodal model Prototypes, low volume, unusual layouts

For a solopreneur or small ops team processing 50–2,000 documents a month across many suppliers, the middle option — layout OCR feeding a JSON-schema-constrained LLM — is the one that actually holds up. Rule templates die every time a vendor changes their footer. Pure vision models cost more per page and hallucinate line items on dense tables.

The 6-step quote-to-invoice extraction workflow

Here's the pipeline I've deployed for clients doing 200–1,500 quotes/month. Every step exists because I've been burned by skipping it.

  1. Ingest. Watch an inbox (ap@company.com), a Drive folder, or a webhook from your portal. Deduplicate on file hash — vendors resend the same PDF constantly.
  2. Normalize. Convert everything to PDF, split multi-quote PDFs, deskew phone photos, upscale scans below 200 DPI. Skip this and your accuracy drops 10–15 points on scanned docs.
  3. OCR with layout. Run a layout-aware engine (AWS Textract, Google Document AI, Azure Document Intelligence, or open-source docTR / PaddleOCR + Unstructured). Keep bounding boxes — you need them for line-item tables.
  4. Structured extraction. Feed the OCR output to an LLM with a strict JSON schema and the raw text. Use function calling or structured outputs so the model can't return prose.
  5. Validate. Math checks (line totals sum to subtotal, subtotal + tax = grand total), currency check, date sanity, vendor lookup against your master list.
  6. Human-in-the-loop for low-confidence. Anything failing validation or below a confidence threshold goes to a review queue. Everything else auto-posts to QuickBooks, Xero, NetSuite, or your ERP.

The unglamorous parts — steps 2 and 5 — are where most in-house builds fail. Founders spend three weeks tuning the LLM prompt and skip the deskew step, then wonder why phone photos have 60% accuracy.

Accuracy benchmarks you can actually expect

I'm going to give you ranges from real production pipelines, not vendor marketing decks. Numbers depend heavily on document quality and how you define "accurate" (field-level vs document-level).

Field-level accuracy (single field correct):

  • Header fields (vendor name, invoice number, date, totals) on clean digital PDFs: 97–99%
  • Header fields on decent scans (300 DPI): 93–97%
  • Header fields on phone photos: 80–92%
  • Line-item extraction (description + qty + unit price + total) on clean PDFs: 90–96%
  • Line items on dense multi-page tables: 75–88%
  • Tax breakdown per line (needed for VAT/multi-rate US sales tax): 70–85%

Document-level accuracy (every field on the document correct — this is what your accountant cares about) drops to 60–85% even on good pipelines, because a 3-line-item quote has ~15 fields and one wrong field breaks the document.

This is why the "just use GPT-5/Claude/Gemini vision" pitch is misleading. Single-document accuracy on a hero example is 98%. Run it on 500 real supplier documents and document-level accuracy sits in the low 80s without validation and review. Google's own Document AI documentation is refreshingly honest about this — their benchmarks are field-level, not document-level. Worth reading before you set expectations with a client: Google Document AI docs.

The right question isn't "what's the accuracy?" It's "what's the accuracy on documents that pass validation, and what percentage passes?" A well-tuned pipeline auto-processes 70–85% of documents at >99% accuracy and routes the rest to a human. That's the number to design for.

The field-mapping table (quotes → invoices → your system)

This is the schema I start every project with. Customize per client, but these are the fields that matter for AP automation in the US SMB context.

Field Type Source doc Maps to (QuickBooks/Xero) Notes
vendor_name string Header Vendor Fuzzy-match against master list
vendor_tax_id string Header/footer Vendor.EIN US: EIN format XX-XXXXXXX
document_type enum Inferred Quote / Bill quote, invoice, credit_note
document_number string Header Ref number Dedup key with vendor
issue_date ISO date Header TxnDate Normalize MM/DD/YY → ISO
due_date ISO date Header DueDate Compute from terms if missing
po_number string Header PONumber Match to open POs
currency ISO 4217 Header/totals CurrencyRef Default USD, detect from symbol
line_items[].description string Body Line.Description
line_items[].quantity decimal Body Line.Qty
line_items[].unit_price decimal Body Line.UnitPrice
line_items[].amount decimal Body Line.Amount Validate qty × unit_price
line_items[].tax_rate decimal Body Line.TaxCode US sales tax varies by state
subtotal decimal Totals Validate sum of line amounts
tax_total decimal Totals TxnTaxDetail
total decimal Totals TotalAmt Must equal subtotal + tax
payment_terms string Footer SalesTermRef Net 30, 2/10 Net 30, etc.
confidence_score float Computed Routing signal

The two fields that trip up every implementation: po_number (vendors put it anywhere) and tax_rate per line (US multi-state sales tax is a mess — your validation layer has to know when a document should have tax and when it shouldn't).

Sample JSON output (what your workflow consumes)

Here's what a well-formed extraction looks like coming out of the LLM step. Keep the schema tight — every optional field is a place hallucinations hide.

{
  "document_id": "doc_01HRZ8K3Q9",
  "document_type": "quote",
  "document_number": "Q-2026-04471",
  "vendor": {
    "name": "Northwind Fabrication LLC",
    "tax_id": "84-1729033",
    "email": "ap@northwindfab.com"
  },
  "issue_date": "2026-06-28",
  "due_date": "2026-07-28",
  "po_number": "PO-88213",
  "currency": "USD",
  "line_items": [
    {
      "line_no": 1,
      "description": "1/4\" steel plate, laser cut, 24x36",
      "quantity": 12,
      "unit_price": 84.50,
      "amount": 1014.00,
      "tax_rate": 0.0825
    },
    {
      "line_no": 2,
      "description": "Powder coat, matte black",
      "quantity": 12,
      "unit_price": 22.00,
      "amount": 264.00,
      "tax_rate": 0.0825
    }
  ],
  "subtotal": 1278.00,
  "tax_total": 105.44,
  "total": 1383.44,
  "payment_terms": "Net 30",
  "validation": {
    "math_check": "pass",
    "vendor_match": "exact",
    "duplicate_check": "pass"
  },
  "confidence": {
    "overall": 0.94,
    "header": 0.98,
    "line_items": 0.91,
    "totals": 0.99
  },
  "routing": "auto_post"
}

Two things worth stealing from this schema: the validation block is populated by your code, not the LLM (LLMs can't do arithmetic reliably), and routing is a computed field that decides whether the document auto-posts or goes to review.

A minimal working extractor in Python

Enough talk. Here's a skeleton that runs. Uses Textract for OCR and an LLM with structured outputs for extraction. Swap providers freely — the shape of the code doesn't change.

import json
import boto3
from pydantic import BaseModel, Field
from openai import OpenAI

class LineItem(BaseModel):
    description: str
    quantity: float
    unit_price: float
    amount: float
    tax_rate: float | None = None

class Quote(BaseModel):
    document_type: str = Field(pattern="^(quote|invoice|credit_note)
quot;) document_number: str vendor_name: str issue_date: str # ISO 8601 currency: str line_items: list[LineItem] subtotal: float tax_total: float total: float def ocr_document(file_bytes: bytes) -> str: textract = boto3.client("textract") resp = textract.analyze_document( Document={"Bytes": file_bytes}, FeatureTypes=["TABLES", "FORMS"], ) lines = [b["Text"] for b in resp["Blocks"] if b["BlockType"] == "LINE"] return "\n".join(lines) def extract_fields(ocr_text: str) -> Quote: client = OpenAI() resp = client.responses.parse( model="gpt-5", # use whatever's current input=[ {"role": "system", "content": "Extract fields from this quote/invoice. " "If a field is not present, do not invent it."}, {"role": "user", "content": ocr_text}, ], text_format=Quote, ) return resp.output_parsed def validate(q: Quote) -> dict: checks = {} line_sum = round(sum(li.amount for li in q.line_items), 2) checks["line_sum_matches_subtotal"] = abs(line_sum - q.subtotal) < 0.02 checks["totals_math"] = abs(q.subtotal + q.tax_total - q.total) < 0.02 for li in q.line_items: expected = round(li.quantity * li.unit_price, 2) if abs(expected - li.amount) > 0.02: checks[f"line_{li.description[:20]}_math"] = False return checks def process(file_bytes: bytes): text = ocr_document(file_bytes) quote = extract_fields(text) checks = validate(quote) auto_post = all(checks.values()) return {"data": quote.model_dump(), "checks": checks, "auto_post": auto_post}

That's the whole spine. The rest of a production system — deduplication, vendor matching, review UI, retry queue, ERP push — is plumbing around this core.

Where this breaks in production (and what to do)

Six failure modes I see repeatedly:

  1. Multi-page invoices with continued tables. OCR splits the table, extraction misses lines on page 2. Fix: stitch tables by column header signature before extracting.
  2. Handwritten annotations on scans. "Add 5% rush fee" scrawled in the margin. Ignore, but surface a warning to the reviewer.
  3. Vendor sends both a quote and an invoice for the same job. Your dedup key needs (vendor, document_number, document_type), not just document number.
  4. Currency ambiguity. $ could be USD, CAD, AUD. Detect from vendor country if not explicit.
  5. Tax lines that aren't line items. Freight, discount, "misc fee" — these show up in the totals section. Extract them separately or your math validation fails.
  6. Zero-dollar or negative line items. Legitimate (credits, freebies) but they crash naive validators. Handle explicitly.

The pattern: every one of these is caught by validation, not by a better LLM. Spend your effort there.

How BizFlowAI approaches this

The AP automation systems we build for clients follow the pipeline above almost exactly — with two tweaks that matter. First, we always start with a two-week measurement phase on the client's real document mix before writing any extraction code. Volumes, vendor count, quote-vs-invoice ratio, and scan-vs-digital split drive every architectural decision. A 200-document/month shop with 8 vendors gets a very different build than a 2,000-document/month distributor with 400 vendors. Second, the review queue is treated as a first-class product, not an afterthought. If your ops person doesn't want to open it, the whole system fails.

We ship with the auto-post rate, average time-to-post, and reviewer-touched-per-document metrics on a dashboard from day one. When a solopreneur asks "is this working?", those three numbers answer it.


Work with BizFlowAI

If you'd rather have this built for you, that's what we do: production AI automation for solo founders and small teams — agents, integrations, and document pipelines that actually ship.

Book a free discovery call — 30 minutes, we map the highest-ROI automation in your workflow. No pitch deck, just engineering.

More guides like this on the BizFlowAI blog.

Frequently asked questions

What is quotation OCR and how does it work?

Quotation OCR converts supplier quotes and invoices from PDFs, scans, or phone photos into structured data like vendor, line items, and totals that flow into ERP or accounting systems. Modern pipelines combine a layout-aware OCR engine (which extracts text plus bounding-box positions) with an LLM or trained model that maps that text to a strict JSON schema. This two-layer approach outperforms raw Tesseract or pasting into ChatGPT because it preserves table structure and enforces validation. It's the standard architecture for automating accounts payable at SMB scale.

How accurate is LLM-based invoice extraction in production?

On clean digital PDFs, field-level accuracy for header fields (vendor, date, totals) runs 97–99%, dropping to 80–92% on phone photos. Line-item extraction hits 90–96% on clean docs but falls to 75–88% on dense multi-page tables. Document-level accuracy (every field correct on a document) typically sits at 60–85% without validation. Well-tuned pipelines auto-process 70–85% of documents at >99% accuracy and route the rest to human review.

Should I use vision LLMs directly or layout OCR plus an LLM for invoices?

For most SMBs processing 50–2,000 mixed-vendor documents per month, layout OCR (Textract, Google Document AI, Azure Document Intelligence, or open-source docTR/PaddleOCR) feeding a JSON-schema-constrained LLM is the most reliable architecture. Pure vision LLMs cost more per page and hallucinate line items on dense tables, making them best for prototypes or low volume. Rule/template OCR only works if you have few vendors with stable layouts. The layout+LLM hybrid balances cost, accuracy, and vendor variability.

What fields should I extract from a quote or invoice for AP automation?

The core schema includes vendor_name, vendor_tax_id (EIN), document_type (quote/invoice/credit_note), document_number, issue_date, due_date, po_number, currency, plus line_items with description, quantity, unit_price, amount, and tax_rate. Totals need subtotal, tax_total, and total, along with payment_terms. Add a computed confidence_score for routing decisions. The two fields that consistently cause problems are po_number (vendors place it inconsistently) and per-line tax_rate (especially with US multi-state sales tax).

Why do invoice OCR pipelines fail even when the LLM is good?

Most failures come from skipping normalization and validation, not from the LLM itself. If you don't deskew phone photos, upscale scans below 200 DPI, and split multi-quote PDFs, accuracy drops 10–15 points on scanned documents. And LLMs can't do arithmetic reliably, so you need code-based validation (line totals sum to subtotal, subtotal + tax = grand total, vendor lookup, duplicate check) to catch errors. Without a human-in-the-loop review queue for low-confidence documents, bad data reaches your accounting system.