Invoice Extractor
A structured methodology for turning an invoice, receipt, or bill — a PDF, a
scanned image, a phone photo, or pasted text — into one clean, validated
JSON object: vendor and bill-to details, line items, totals, tax/VAT, and
payment terms. The skill checks the arithmetic (line items sum to subtotal,
subtotal − discount + tax + shipping = total, amount due = total − paid) and
emits a validation block that flags every discrepancy instead of silently
"fixing" the numbers.
Verify before you pay. Extracted values can be wrong — OCR misreads a
digit, a stamped "PAID" overlaps a number, a multi-page total is split. Always
verify extracted values against the source document before posting to your
ledger or paying. Treat any field marked low-confidence as needing human
review. This is a data-entry aid, not financial or legal advice.
When to Activate
Activate this skill when the user:
- Shares an invoice, receipt, bill, purchase order, credit note, or proforma
as a PDF, image (PNG/JPG/HEIC), scan, photo, or pasted text and asks to
extract, parse, digitize, read, or "pull the data out of" it.
- Asks for accounts-payable automation, bookkeeping data entry, expense
processing, or "turn this receipt into a spreadsheet/JSON".
- Wants line items, totals, tax, or vendor details lifted into a structured
format, or asks "does this invoice add up?".
If the document is unreadable, partly cut off, or a key total is illegible, say
so and extract what is legible — never fabricate a number to make the math
balance. Mark anything guessed or derived with low confidence.
Step 1: Identify Document Type
Before extracting, classify the document and note its quality — both change how
you read it and how much to trust each field.
Document type (state which, and why):
| Type |
Tell |
| Invoice |
"Invoice", an invoice number, an amount due and due date — a demand for payment. |
| Receipt |
Proof of a completed payment — "Paid", tender/change, often a register/POS layout. |
| Purchase order (PO) |
Buyer-issued, a "PO number", ordered quantities, often no tax/amount-due. |
| Credit note |
Negative/refund document — "Credit", "Credit memo", reverses a prior invoice. |
| Proforma |
Quote/estimate issued before the sale — "Proforma", "Quotation", not yet payable. |
Quality flags (carry into confidence): scanned vs native PDF, phone photo
(skew/glare/shadow), handwritten fields, faint thermal receipt, multi-page (note
which page totals live on), non-English or mixed-language, multiple currencies.
Step 2: Field Extraction
Pull these fields. Anything absent → null (do not invent). Anything derived,
guessed, or OCR-ambiguous → record a confidence entry (Step 5).
Header
- Vendor / supplier name, address, tax / VAT / GST id, phone/email.
- Bill-to (and ship-to if different): name + address.
- Invoice / document number, PO reference (if any).
- Issue date, due date.
- Currency (the symbol or code shown on the document).
Line items (one object per line):
- Description, quantity, unit price, line amount.
- Tax rate / tax category per line, if shown (e.g.
20%, VAT,
Exempt, Zero-rated). If tax is only stated at the document level, leave
line-level tax null and capture it in totals.
Totals
- Subtotal (pre-tax net), discount, tax / VAT, shipping /
freight, grand total.
- Amount paid, amount due / balance (for receipts, paid = total and due
= 0; for invoices, due is what's owed).
Payment
- Terms (e.g.
Net 30, Due on receipt, 2/10 Net 30).
- Bank details if present: bank name, IBAN, account / routing /
sort code, SWIFT/BIC. Accepted payment methods (card, ACH, wire, etc.).
Step 3: Normalization Rules
Normalize as you extract so the JSON is machine-clean — but preserve the
original string wherever a transform could be wrong.
- Dates → ISO 8601 (
YYYY-MM-DD). When the format is ambiguous (e.g.
03/04/2025 — is it Mar 4 or Apr 3?), use document locale/currency as a hint,
flag it low-confidence, and keep the raw string in confidence.note.
- Amounts → plain decimals. Strip thousands separators and currency symbols;
keep the decimal point.
$1,234.50 → 1234.50. Respect locale decimal commas
(1.234,50 → 1234.50). Negative/refund amounts stay negative.
- Currency → ISO 4217 code (
$ → USD or CAD/AUD per context, € →
EUR, £ → GBP). If the symbol is ambiguous, keep the symbol and flag it.
- Map synonyms to the canonical field: "Total Due" / "Balance Due" /
"Amount Payable" / "Please Pay" →
amount_due; "Sub-total" / "Net" →
subtotal; "VAT" / "GST" / "Sales Tax" → tax; "Bill To" / "Sold To" →
bill_to.
- Preserve original strings where ambiguous — never overwrite a raw value
you transformed with low confidence; keep both.
Step 4: Validation & Reconciliation
Run every applicable check and record the result. Never silently adjust a
number to make a check pass — flag it. Allow a small rounding tolerance
(±0.02 of the document currency, or per-line rounding) before calling a
mismatch.
| Check |
Rule |
| Line items → subtotal |
Σ(line amounts) = subtotal |
| Line math |
quantity × unit_price = line_amount (per line) |
| Subtotal → total |
subtotal − discount + tax + shipping = grand_total |
| Tax |
tax ≈ subtotal × tax_rate (within rounding), where a rate is given |
| Amount due |
amount_due = grand_total − amount_paid |
For each check emit a validation entry: the check name, status of pass
or fail, and on failure the expected, actual, and discrepancy (signed
difference) so a human can see exactly what's off. If a check can't run (a
needed field is missing), mark it status: "skipped" with a reason.
Step 5: Output Format
Emit, in this order: (1) a strict JSON object, (2) a short human-readable
summary, (3) a flagged-issues list.
JSON shape
{
"document_type": "invoice | receipt | purchase_order | credit_note | proforma",
"currency": "ISO 4217 code",
"vendor": {
"name": "string|null",
"address": "string|null",
"tax_id": "string|null",
"email": "string|null",
"phone": "string|null"
},
"bill_to": { "name": "string|null", "address": "string|null" },
"ship_to": { "name": "string|null", "address": "string|null" },
"invoice_number": "string|null",
"po_reference": "string|null",
"issue_date": "YYYY-MM-DD|null",
"due_date": "YYYY-MM-DD|null",
"line_items": [
{
"description": "string",
"quantity": 0,
"unit_price": 0.00,
"line_amount": 0.00,
"tax_rate": "string|null",
"tax_category": "string|null"
}
],
"totals": {
"subtotal": 0.00,
"discount": 0.00,
"tax": 0.00,
"shipping": 0.00,
"grand_total": 0.00,
"amount_paid": 0.00,
"amount_due": 0.00
},
"payment": {
"terms": "string|null",
"methods": ["string"],
"bank": {
"bank_name": "string|null",
"iban": "string|null",
"account_number": "string|null",
"routing_or_sort_code": "string|null",
"swift_bic": "string|null"
}
},
"validation": [
{
"check": "line_items_sum_to_subtotal",
"status": "pass | fail | skipped",
"expected": 0.00,
"actual": 0.00,
"discrepancy": 0.00,
"reason": "string (only when skipped)"
}
],
"confidence": [
{ "field": "dotted.path", "level": "high | medium | low", "note": "why" }
]
}
Rules: omit confidence entries for fields read cleanly — list only low/medium
ones (and any field you transformed under ambiguity). Use null for absent
fields, never "" or 0 as a stand-in for "unknown". Keep numbers as JSON
numbers, not strings.
Inline example A — clean receipt (all checks pass)
{
"document_type": "receipt",
"currency": "USD",
"vendor": { "name": "Blue Bottle Coffee", "address": "12 Mint St, San Francisco, CA 94103", "tax_id": null, "email": null, "phone": null },
"bill_to": { "name": null, "address": null },
"invoice_number": "R-558210",
"issue_date": "2025-05-18",
"due_date": null,
"line_items": [
{ "description": "Cappuccino", "quantity": 2, "unit_price": 4.50, "line_amount": 9.00, "tax_rate": null, "tax_category": null },
{ "description": "Almond croissant", "quantity": 1, "unit_price": 5.00, "line_amount": 5.00, "tax_rate": null, "tax_category": null }
],
"totals": { "subtotal": 14.00, "discount": 0.00, "tax": 1.23, "shipping": 0.00, "grand_total": 15.23, "amount_paid": 15.23, "amount_due": 0.00 },
"payment": { "terms": "Paid", "methods": ["card"], "bank": null },
"validation": [
{ "check": "line_items_sum_to_subtotal", "status": "pass", "expected": 14.00, "actual": 14.00, "discrepancy": 0.00 },
{ "check": "subtotal_minus_discount_plus_tax_plus_shipping_equals_total", "status": "pass", "expected": 15.23, "actual": 15.23, "discrepancy": 0.00 },
{ "check": "amount_due_equals_total_minus_paid", "status": "pass", "expected": 0.00, "actual": 0.00, "discrepancy": 0.00 }
]
}
Inline example B — invoice with a flagged subtotal mismatch
{
"document_type": "invoice",
"currency": "EUR",
"vendor": { "name": "Hektar Supplies GmbH", "address": "Industriestr. 9, 80807 München", "tax_id": "DE811209227", "email": "billing@hektar.de", "phone": null },
"bill_to": { "name": "Acme Robotics Ltd", "address": "Unit 4, Park Road, Leeds LS1 2AB" },
"invoice_number": "2025-0412",
"po_reference": "PO-7781",
"issue_date": "2025-04-03",
"due_date": "2025-05-03",
"line_items": [
{ "description": "M6 hex bolts (box/500)", "quantity": 10, "unit_price": 12.00, "line_amount": 120.00, "tax_rate": "19%", "tax_category": "VAT" },
{ "description": "Threadlocker 50ml", "quantity": 4, "unit_price": 8.50, "line_amount": 34.00, "tax_rate": "19%", "tax_category": "VAT" }
],
"totals": { "subtotal": 160.00, "discount": 0.00, "tax": 29.26, "shipping": 0.00, "grand_total": 183.26, "amount_paid": 0.00, "amount_due": 183.26 },
"payment": { "terms": "Net 30", "methods": ["wire"], "bank": { "bank_name": "Stadtsparkasse München", "iban": "DE89370400440532013000", "swift_bic": "SSKMDEMM", "account_number": null, "routing_or_sort_code": null } },
"validation": [
{ "check": "line_items_sum_to_subtotal", "status": "fail", "expected": 154.00, "actual": 160.00, "discrepancy": 6.00 },
{ "check": "subtotal_minus_discount_plus_tax_plus_shipping_equals_total", "status": "fail", "expected": 189.26, "actual": 183.26, "discrepancy": -6.00 },
{ "check": "tax_equals_subtotal_times_rate", "status": "fail", "expected": 30.40, "actual": 29.26, "discrepancy": -1.14 }
],
"confidence": [
{ "field": "totals.subtotal", "level": "low", "note": "Line items sum to 154.00 but printed subtotal is 160.00 — printed subtotal kept; mismatch flagged for review." }
]
}
Human-readable summary
After the JSON, add 2–4 lines: document type, vendor, total + currency, due
date/terms, and whether validation passed. Then a flagged-issues list —
every failed/skipped check and every low-confidence field, most material first,
each with the numbers behind it.
Disclaimer
This skill is a document data-entry aid, not financial, accounting, tax, or
legal advice. Extraction depends on document quality: OCR misreads, skewed
scans, handwriting, overlapping stamps, and layout ambiguity all cause errors.
Verify extracted values against the source document before posting to a ledger
or paying anything, and treat low-confidence fields as requiring human review.
The validation block flags arithmetic discrepancies — it does not certify that a
document is genuine or that a charge is correct. This skill is not affiliated
with or endorsed by Anthropic or any accounting-software provider.
1---2name: invoice-extractor3description: Extract structured data from invoices, receipts, and bills into clean validated JSON — vendor, line items, totals, tax, and payment terms. Validates the math and flags discrepancies. Use for accounts-payable automation, bookkeeping, expense processing, or document data entry.4---56# Invoice Extractor78A structured methodology for turning an invoice, receipt, or bill — a PDF, a9scanned image, a phone photo, or pasted text — into one clean, **validated**10JSON object: vendor and bill-to details, line items, totals, tax/VAT, and11payment terms. The skill **checks the arithmetic** (line items sum to subtotal,12subtotal − discount + tax + shipping = total, amount due = total − paid) and13emits a `validation` block that flags every discrepancy instead of silently14"fixing" the numbers.1516> **Verify before you pay.** Extracted values can be wrong — OCR misreads a17> digit, a stamped "PAID" overlaps a number, a multi-page total is split. Always18> verify extracted values against the source document before posting to your19> ledger or paying. Treat any field marked low-confidence as needing human20> review. This is a data-entry aid, not financial or legal advice.2122---2324## When to Activate2526Activate this skill when the user:2728- Shares an **invoice, receipt, bill, purchase order, credit note, or proforma**29 as a PDF, image (PNG/JPG/HEIC), scan, photo, or pasted text and asks to30 extract, parse, digitize, read, or "pull the data out of" it.31- Asks for accounts-payable automation, bookkeeping data entry, expense32 processing, or "turn this receipt into a spreadsheet/JSON".33- Wants line items, totals, tax, or vendor details lifted into a structured34 format, or asks "does this invoice add up?".3536If the document is unreadable, partly cut off, or a key total is illegible, say37so and extract what is legible — **never fabricate a number to make the math38balance.** Mark anything guessed or derived with low confidence.3940---4142## Step 1: Identify Document Type4344Before extracting, classify the document and note its quality — both change how45you read it and how much to trust each field.4647**Document type** (state which, and why):4849| Type | Tell |50|---|---|51| **Invoice** | "Invoice", an invoice number, an amount **due** and due date — a demand for payment. |52| **Receipt** | Proof of a **completed** payment — "Paid", tender/change, often a register/POS layout. |53| **Purchase order (PO)** | Buyer-issued, a "PO number", **ordered** quantities, often no tax/amount-due. |54| **Credit note** | Negative/refund document — "Credit", "Credit memo", reverses a prior invoice. |55| **Proforma** | Quote/estimate issued **before** the sale — "Proforma", "Quotation", not yet payable. |5657**Quality flags** (carry into `confidence`): scanned vs native PDF, phone photo58(skew/glare/shadow), handwritten fields, faint thermal receipt, multi-page (note59which page totals live on), non-English or mixed-language, multiple currencies.6061---6263## Step 2: Field Extraction6465Pull these fields. Anything absent → `null` (do not invent). Anything derived,66guessed, or OCR-ambiguous → record a `confidence` entry (Step 5).6768**Header**69- Vendor / supplier **name**, **address**, **tax / VAT / GST id**, phone/email.70- **Bill-to** (and **ship-to** if different): name + address.71- **Invoice / document number**, **PO reference** (if any).72- **Issue date**, **due date**.73- **Currency** (the symbol or code shown on the document).7475**Line items** (one object per line):76- **Description**, **quantity**, **unit price**, **line amount**.77- **Tax rate** / **tax category** per line, if shown (e.g. `20%`, `VAT`,78 `Exempt`, `Zero-rated`). If tax is only stated at the document level, leave79 line-level tax `null` and capture it in `totals`.8081**Totals**82- **Subtotal** (pre-tax net), **discount**, **tax / VAT**, **shipping /83 freight**, **grand total**.84- **Amount paid**, **amount due / balance** (for receipts, paid = total and due85 = 0; for invoices, due is what's owed).8687**Payment**88- **Terms** (e.g. `Net 30`, `Due on receipt`, `2/10 Net 30`).89- **Bank details** if present: bank name, **IBAN**, **account / routing /90 sort code**, SWIFT/BIC. **Accepted payment methods** (card, ACH, wire, etc.).9192---9394## Step 3: Normalization Rules9596Normalize as you extract so the JSON is machine-clean — but **preserve the97original string** wherever a transform could be wrong.9899- **Dates → ISO 8601** (`YYYY-MM-DD`). When the format is ambiguous (e.g.100 `03/04/2025` — is it Mar 4 or Apr 3?), use document locale/currency as a hint,101 flag it low-confidence, and keep the raw string in `confidence.note`.102- **Amounts → plain decimals.** Strip thousands separators and currency symbols;103 keep the decimal point. `$1,234.50` → `1234.50`. Respect locale decimal commas104 (`1.234,50` → `1234.50`). Negative/refund amounts stay negative.105- **Currency → ISO 4217 code** (`$` → `USD` or `CAD`/`AUD` per context, `€` →106 `EUR`, `£` → `GBP`). If the symbol is ambiguous, keep the symbol and flag it.107- **Map synonyms to the canonical field:** "Total Due" / "Balance Due" /108 "Amount Payable" / "Please Pay" → `amount_due`; "Sub-total" / "Net" →109 `subtotal`; "VAT" / "GST" / "Sales Tax" → `tax`; "Bill To" / "Sold To" →110 `bill_to`.111- **Preserve original strings where ambiguous** — never overwrite a raw value112 you transformed with low confidence; keep both.113114---115116## Step 4: Validation & Reconciliation117118Run every applicable check and record the result. **Never silently adjust a119number to make a check pass — flag it.** Allow a small rounding tolerance120(±0.02 of the document currency, or per-line rounding) before calling a121mismatch.122123| Check | Rule |124|---|---|125| **Line items → subtotal** | Σ(line amounts) = subtotal |126| **Line math** | quantity × unit_price = line_amount (per line) |127| **Subtotal → total** | subtotal − discount + tax + shipping = grand_total |128| **Tax** | tax ≈ subtotal × tax_rate (within rounding), where a rate is given |129| **Amount due** | amount_due = grand_total − amount_paid |130131For each check emit a `validation` entry: the `check` name, `status` of `pass`132or `fail`, and on failure the `expected`, `actual`, and `discrepancy` (signed133difference) so a human can see exactly what's off. If a check can't run (a134needed field is missing), mark it `status: "skipped"` with a `reason`.135136---137138## Step 5: Output Format139140Emit, in this order: (1) a strict JSON object, (2) a short human-readable141summary, (3) a flagged-issues list.142143### JSON shape144145```json146{147 "document_type": "invoice | receipt | purchase_order | credit_note | proforma",148 "currency": "ISO 4217 code",149 "vendor": {150 "name": "string|null",151 "address": "string|null",152 "tax_id": "string|null",153 "email": "string|null",154 "phone": "string|null"155 },156 "bill_to": { "name": "string|null", "address": "string|null" },157 "ship_to": { "name": "string|null", "address": "string|null" },158 "invoice_number": "string|null",159 "po_reference": "string|null",160 "issue_date": "YYYY-MM-DD|null",161 "due_date": "YYYY-MM-DD|null",162 "line_items": [163 {164 "description": "string",165 "quantity": 0,166 "unit_price": 0.00,167 "line_amount": 0.00,168 "tax_rate": "string|null",169 "tax_category": "string|null"170 }171 ],172 "totals": {173 "subtotal": 0.00,174 "discount": 0.00,175 "tax": 0.00,176 "shipping": 0.00,177 "grand_total": 0.00,178 "amount_paid": 0.00,179 "amount_due": 0.00180 },181 "payment": {182 "terms": "string|null",183 "methods": ["string"],184 "bank": {185 "bank_name": "string|null",186 "iban": "string|null",187 "account_number": "string|null",188 "routing_or_sort_code": "string|null",189 "swift_bic": "string|null"190 }191 },192 "validation": [193 {194 "check": "line_items_sum_to_subtotal",195 "status": "pass | fail | skipped",196 "expected": 0.00,197 "actual": 0.00,198 "discrepancy": 0.00,199 "reason": "string (only when skipped)"200 }201 ],202 "confidence": [203 { "field": "dotted.path", "level": "high | medium | low", "note": "why" }204 ]205}206```207208Rules: omit `confidence` entries for fields read cleanly — list only low/medium209ones (and any field you transformed under ambiguity). Use `null` for absent210fields, never `""` or `0` as a stand-in for "unknown". Keep numbers as JSON211numbers, not strings.212213### Inline example A — clean receipt (all checks pass)214215```json216{217 "document_type": "receipt",218 "currency": "USD",219 "vendor": { "name": "Blue Bottle Coffee", "address": "12 Mint St, San Francisco, CA 94103", "tax_id": null, "email": null, "phone": null },220 "bill_to": { "name": null, "address": null },221 "invoice_number": "R-558210",222 "issue_date": "2025-05-18",223 "due_date": null,224 "line_items": [225 { "description": "Cappuccino", "quantity": 2, "unit_price": 4.50, "line_amount": 9.00, "tax_rate": null, "tax_category": null },226 { "description": "Almond croissant", "quantity": 1, "unit_price": 5.00, "line_amount": 5.00, "tax_rate": null, "tax_category": null }227 ],228 "totals": { "subtotal": 14.00, "discount": 0.00, "tax": 1.23, "shipping": 0.00, "grand_total": 15.23, "amount_paid": 15.23, "amount_due": 0.00 },229 "payment": { "terms": "Paid", "methods": ["card"], "bank": null },230 "validation": [231 { "check": "line_items_sum_to_subtotal", "status": "pass", "expected": 14.00, "actual": 14.00, "discrepancy": 0.00 },232 { "check": "subtotal_minus_discount_plus_tax_plus_shipping_equals_total", "status": "pass", "expected": 15.23, "actual": 15.23, "discrepancy": 0.00 },233 { "check": "amount_due_equals_total_minus_paid", "status": "pass", "expected": 0.00, "actual": 0.00, "discrepancy": 0.00 }234 ]235}236```237238### Inline example B — invoice with a flagged subtotal mismatch239240```json241{242 "document_type": "invoice",243 "currency": "EUR",244 "vendor": { "name": "Hektar Supplies GmbH", "address": "Industriestr. 9, 80807 München", "tax_id": "DE811209227", "email": "billing@hektar.de", "phone": null },245 "bill_to": { "name": "Acme Robotics Ltd", "address": "Unit 4, Park Road, Leeds LS1 2AB" },246 "invoice_number": "2025-0412",247 "po_reference": "PO-7781",248 "issue_date": "2025-04-03",249 "due_date": "2025-05-03",250 "line_items": [251 { "description": "M6 hex bolts (box/500)", "quantity": 10, "unit_price": 12.00, "line_amount": 120.00, "tax_rate": "19%", "tax_category": "VAT" },252 { "description": "Threadlocker 50ml", "quantity": 4, "unit_price": 8.50, "line_amount": 34.00, "tax_rate": "19%", "tax_category": "VAT" }253 ],254 "totals": { "subtotal": 160.00, "discount": 0.00, "tax": 29.26, "shipping": 0.00, "grand_total": 183.26, "amount_paid": 0.00, "amount_due": 183.26 },255 "payment": { "terms": "Net 30", "methods": ["wire"], "bank": { "bank_name": "Stadtsparkasse München", "iban": "DE89370400440532013000", "swift_bic": "SSKMDEMM", "account_number": null, "routing_or_sort_code": null } },256 "validation": [257 { "check": "line_items_sum_to_subtotal", "status": "fail", "expected": 154.00, "actual": 160.00, "discrepancy": 6.00 },258 { "check": "subtotal_minus_discount_plus_tax_plus_shipping_equals_total", "status": "fail", "expected": 189.26, "actual": 183.26, "discrepancy": -6.00 },259 { "check": "tax_equals_subtotal_times_rate", "status": "fail", "expected": 30.40, "actual": 29.26, "discrepancy": -1.14 }260 ],261 "confidence": [262 { "field": "totals.subtotal", "level": "low", "note": "Line items sum to 154.00 but printed subtotal is 160.00 — printed subtotal kept; mismatch flagged for review." }263 ]264}265```266267### Human-readable summary268269After the JSON, add 2–4 lines: document type, vendor, total + currency, due270date/terms, and whether validation passed. Then a **flagged-issues list** —271every failed/skipped check and every low-confidence field, most material first,272each with the numbers behind it.273274---275276## Disclaimer277278This skill is a **document data-entry aid, not financial, accounting, tax, or279legal advice.** Extraction depends on document quality: OCR misreads, skewed280scans, handwriting, overlapping stamps, and layout ambiguity all cause errors.281**Verify extracted values against the source document before posting to a ledger282or paying anything**, and treat low-confidence fields as requiring human review.283The validation block flags arithmetic discrepancies — it does not certify that a284document is genuine or that a charge is correct. This skill is **not affiliated285with or endorsed by Anthropic** or any accounting-software provider.