Prompt Engineering for Invoice Line-Item Extraction With LLMs
Your header-field extraction works. Invoice number, vendor, date, and grand total come back correct on almost every document you throw at it. Then you add line items and the accuracy falls off a cliff: eleven rows on the PDF, nine rows in the JSON, a discount line silently folded into the row above it, and a subtotal that got extracted as if it were a product. The instinct is to write a longer prompt. That almost never fixes it.
Short Answer
Line-item prompts fail because the model has no way to know it made a mistake. Adding more instructions does not create that feedback loop. What does is an arithmetic self-check (quantity times unit price equals line total, and the sum of line totals plus tax and shipping minus discounts equals the invoice grand total), plus an explicit rule that missing values return null rather than a guess. Validate the arithmetic in your code, not just in the prompt, and route mismatches to a human.
Where Line-Item Prompts Actually Break
| Failure mode | What you see in the output | Prompt-only fix | What actually works |
|---|---|---|---|
| Totals do not reconcile | Rows extracted, sum is off by one line | Weak | Compute the sum in code, compare to the stated grand total, flag the delta |
| Discount rows | Discount absorbed into the previous row, or extracted as a product | Weak | A row_type enum: item, discount, subtotal, tax, shipping, freight |
| Continuation across pages | Rows on page 2 dropped, or the repeated column header extracted as a row | Weak | Extract per page with a running index, then merge and de-duplicate in code |
| Missing fields | Model invents a plausible unit price | Moderate | Nullable schema fields plus "never infer" instruction plus a reason note |
| Merged or spanning cells | Category label becomes a line item | Weak | row_type enum plus a required numeric line_total on item rows |
| Column drift | Quantity and unit price swapped on some invoices | Moderate | Arithmetic check catches the swap when qty x price no longer equals the total |
The pattern is consistent. The prompt can fix failures where the model needs a definition. It cannot fix failures where the model needs to be checked. Most teams spend weeks on the first kind when the win is in the second.
Have a messy PDF? Upload 1-3 sample pages and we will tell you if it is clean, OCR-heavy, or needs human QA.
Arithmetic Self-Checks Are the Core Technique
An invoice is one of the few document types that carries its own answer key. The vendor already did the math and printed it. Use it.
Ask the model to return, alongside the rows, a small reconciliation object:
{
"line_items": [...],
"reconciliation": {
"computed_line_total_sum": 4820.00,
"stated_subtotal": 4820.00,
"stated_tax": 385.60,
"stated_shipping": 0.00,
"stated_discount_total": -240.00,
"computed_grand_total": 4965.60,
"stated_grand_total": 4965.60,
"reconciles": true
}
}
Two things matter here. First, the model states both the number it computed and the number printed on the page, which forces the comparison into the open. Second, and more importantly, you recompute all of it in your own code from the returned rows. Never trust reconciles: true from the model. Language models are unreliable arithmetic engines, and a model that dropped a row will happily sum the rows it did return and declare success. The model's version is a signal. Your code's version is the gate.
When the two disagree, you have found a document that needs review before it reaches the general ledger. That is the value of the technique: not perfect extraction, but reliable detection of imperfect extraction.
Add per-row checks too. Where quantity, unit price, and line total are all present, verify qty * unit_price = line_total within a rounding tolerance of a cent or two. This catches column drift, misread decimals, and OCR digit errors before they hide inside a total that happens to balance.
Explicit Null Handling Beats Inference
The single highest-value line in a line-item prompt is some version of: if a value is not printed on the document, return null. Do not infer it, do not compute it, do not carry it forward from the row above.
Without that rule, models fill gaps. A service invoice with no quantity column gets "quantity": 1 on every row, which looks harmless until your arithmetic check passes on a fabricated number. A blank unit price gets back-computed from the line total, often right and occasionally wrong in a way nobody catches.
Make the schema support this. Fields that can legitimately be absent must be nullable in the schema itself, not just in the instructions. If your schema requires a number, the model has to produce one, and it will. Pair the nullable field with a short note explaining what was unclear:
{
"description": "Annual maintenance, sites 1-4",
"quantity": null,
"unit_price": null,
"line_total": 12000.00,
"row_type": "item",
"extraction_note": "no quantity or unit price column on this invoice"
}
That note is more useful than a numeric confidence score. Models are poorly calibrated when asked to rate their own certainty from 0 to 1, and downstream you end up picking an arbitrary threshold. A short categorical flag or a plain sentence about what was ambiguous gives a reviewer something actionable. If you do want a structured signal, a small enum such as confident, ambiguous, conflicting, or not_present is easier to act on than a float.
Continuation Rows and Page Boundaries
Multi-page invoices break line-item extraction more often than any other structural feature. The table continues on page 2, the column header repeats, a "carried forward" subtotal appears at the top, and the model has to decide what all of that means.
For short invoices, three to five pages, send the whole document in one request and instruct the model to treat repeated column headers and carried-forward subtotals as structure rather than data. Give it an explicit rule: rows whose description matches the column header text are not line items, and rows labeled "carried forward", "balance forward", "continued", or "subtotal" get row_type: subtotal, not item.
For longer documents, process page by page and stitch in code. Ask for a sequential row_index per page and keep a running offset. Merging becomes a deterministic operation you control, and de-duplication becomes a comparison you can log rather than a judgment call inside a model you cannot inspect. The tradeoff is that a row split across a page break needs a repair step. Handle it by checking for item rows with a description and no line_total at page boundaries.
Either way, the arithmetic check tells you it worked. If page 2 got dropped, the line-item sum will not match the grand total and the document gets flagged.
Structured Output Mode Is Necessary, Not Sufficient
Strict JSON schema modes from the major providers guarantee the response conforms to the shape you defined: every required key present, types matched, enums respected. That eliminates a class of parsing bugs and you should use it.
It guarantees nothing about whether the values are correct. A schema-valid response can contain nine rows when the invoice has eleven, a swapped quantity, or a hallucinated unit price. Constrained decoding restricts the shape of the output, not its relationship to the source document. Teams that adopt structured outputs and then drop their validation layer ship more silent errors than before, because the failures stop announcing themselves as parse exceptions.
Keep both. Schema enforcement for shape, arithmetic reconciliation for truth.
A Prompt Skeleton That Holds Up
Order the instructions so the definitions come before the extraction request:
- Define
row_typeand what belongs in each value. Be explicit that discounts, taxes, shipping, and subtotals are notitemrows. - State the null rule in one unambiguous sentence.
- State the arithmetic requirement: return computed and stated totals separately.
- Give one or two few-shot examples, and make sure at least one contains a discount row and a null field. Examples do more work than paragraphs of prose.
- Ask for the reconciliation object last.
Then, in code: recompute, compare, and route mismatches to review. Length is not the lever. A prompt that fits on one screen with a working validation layer beats a two-page prompt with none.
When to Stop Tuning and Route to Humans
If your reconciliation rate sits above roughly 90 percent, prompt work is worth continuing, because the failures are usually a small number of recurring vendor formats you can address with a targeted example.
If you are under that, and especially if the documents are scanned, faxed, photographed at an angle, or stamped and annotated by hand, the bottleneck is upstream of the prompt. No instruction fixes an image where the decimal point is not legible. At that point either improve the input, with better scans or native PDFs from the vendor, or accept that a review step is part of the pipeline.
If you are building this into a product with steady volume, an invoice-specific platform with a maintained validation layer will get you further than a hand-rolled prompt. Say that plainly to whoever asked you to build it. If instead you have a finite pile of invoices that need to be clean in Excel once, DataConvertPro handles the extraction, the reconciliation, and the human check on the rows that do not balance, and you get the file back rather than a system to maintain.
Frequently Asked Questions
Should I extract line items and header fields in the same request?
Usually yes for invoices under about five pages, because the model needs the grand total in context to perform the reconciliation. Splitting them into separate calls means the line-item pass has nothing to check itself against. For long documents, split by page but always include the summary page containing the totals in the line-item context.
Does asking the model to double-check its own math actually help?
It helps as a signal and not as a guarantee. Asking for computed and stated totals as separate fields surfaces disagreements you would otherwise miss, and it makes the model attend to the numbers rather than pattern-matching a table. But the verdict must be recomputed in your code. A model that dropped a row will sum what it kept and report that everything balances.
How do I handle invoices where quantity and unit price are missing entirely?
Return null for both and keep line_total. Plenty of legitimate invoices, professional services, fixed-fee contracts, milestone billing, have no quantity column at all. Your reconciliation should skip the per-row qty * price check whenever either input is null, and rely on the line-total sum against the grand total instead.
Will a bigger or newer model make this unnecessary?
Better models reduce the frequency of these errors but do not change their character. Dropped continuation rows, misclassified discount lines, and inferred quantities still occur, just less often, and less often is harder to catch than often. The reconciliation layer is what makes the residual failures visible, so it stays useful regardless of which model sits underneath it.
Ready to Convert Your Documents?
Stop wasting time on manual PDF to Excel conversions. Get a free quote and learn how DataConvertPro can handle your document processing needs with AI-assisted extraction and human verification.