P3.14 · D2 + D5 · Process33% of CCA-F26 min build

Invoice Processing Agent.

An AP-automation agent that wraps four guarantees around invoice approval. (1) Forced tool_use with a strict JSON schema (vendor_id, invoice_number, line_items[], total_amount, currency ISO 4217, due_date ISO 8601, PO_reference nullable) prevents fabrication. (2) Validation-retry loop confirms sum(line_items) total, currency in ISO 4217 enum, due_date >= invoice_date. (3) Three-way match reconciles invoice with the purchase order and the goods receipt; variance > 2% routes to human review. (4) PreToolUse hook on approve_payment denies if invoice_amount > vendor_authorization_cap, or if vendor on blocklist, or if (vendor_id, invoice_number) was seen in the last 90 days (duplicate detection). PostToolUse audit log captures every approval and rejection. The most-tested distractor: prompt-only field extraction leaks ~15% on edge invoices; forced tool_choice is the only credible architecture.

Mental modelArchitecture chooses the flow. Deterministic controls enforce policy. Structured state preserves truth.
Loop mascot illustrating Invoice Processing Agent.
Share
01 · System & parts

What this system is, and its 5 parts

Think of this as the agent that handles your accounts-payable inbox without the team paying the same invoice twice. A vendor emails a PDF or scanned image; the agent extracts the structured fields (vendor, invoice number, line items, total, currency, due date, PO reference) using a strict schema so it cannot make values up; then it checks the math (line totals must equal the header total), looks up the matching purchase order and goods receipt to make sure the three documents agree, asks a deterministic policy hook whether the vendor still has authorization headroom and whether this exact invoice has been seen in the last 90 days, and only then approves payment. Anything ambiguous routes to a human AP analyst with a structured exception block. The whole point is that AP automation is one wrong cap-policy or duplicate-detection check away from a real money loss.

5Components
D2Primary domain
8Build steps
9Decision traps
8Concept links
Stack · Claude SDK. Vision-capable model for PDF / image. PO + GRN systems of record. Durable audit log.Needs · Forced tool_choice. Validation-retry. PreToolUse hooks. Three-way match.
Invoice Processing Agent component architecture.
5 components. Each owns one concept.
01

Invoice JSON Schema

the contract, in tools[0].input_schema

The output shape lives inside a tool definition, not as freeform text. Required: vendor_id, invoice_number, line_items[], total_amount, currency (ISO 4217 enum), due_date (ISO 8601 string). Optional and nullable: PO_reference, tax_amount, notes. Every numeric field has a minimum: 0. Every line item has description, quantity, unit_price, total.

Configurationtools = [{ name: 'extract_invoice', input_schema: { type: 'object', properties: { vendor_id: {type: 'string'}, invoice_number: {type: 'string'}, total_amount: {type: 'number', minimum: 0}, currency: {type: 'string', enum: ['USD', 'EUR', 'GBP', 'INR', 'JPY', 'unclear']}, due_date: {type: 'string', format: 'date'}, line_items: {type: 'array', items: {...}}, PO_reference: {type: ['string', 'null']} }, required: ['vendor_id', 'invoice_number', 'total_amount', 'currency', 'due_date', 'line_items'] } }]
Concept: structured-outputs
02

Forced tool_use Extractor

tool_choice: { type: 'tool', name: 'extract_invoice' }

Forces the model to fire extract_invoice with arguments matching the schema. No prose preamble, no probabilistic adherence. Vision-capable invocation reads the PDF or image; the model emits a structured tool_use. Pair with few-shot examples that show currency: 'unclear' on truly ambiguous source.

Configurationtool_choice: { type: 'tool', name: 'extract_invoice' }. Use auto only on triage-style flows. Forced is for mandatory extraction.
Concept: tool-choice
03

Validation-Retry Loop

sum check, currency enum, date sanity

Schema enforces shape. Code enforces meaning. After parse: sum(line_items[].total) total_amount (within 0.01 cent tolerance for FX rounding); currency in the enum; due_date format YYYY-MM-DD; due_date >= invoice_date. On failure, feed the specific error back to the model ('line totals sum to 4950 but header total is 5000'); typical convergence in 1-2 retries.

Configurationloop: extract -> parse -> validate_semantically -> on failure, append { role: 'user', content: tool_result with is_error: true and a specific error } -> retry. Max retries: 3. After 3, route to human review.
Concept: evaluation
04

Three-Way Match Service

invoice + PO + goods receipt

Queries the PO master and the goods-receipt ledger by PO_reference. Compares amount (variance <= 2% OK for FX rounding and small price changes), vendor identity (normalized vendor name fuzzy match), line-item count (must match), and date sanity (invoice date >= PO date; receipt date >= PO date). Variance above thresholds returns a structured exception; invoice is held pending human review.

Configurationmatch(invoice, po, grn) -> { match: bool, variance_pct, mismatched_fields[], routed_to: 'auto-approve' | 'human-review' }. Threshold: amount variance > 2% -> human-review. Vendor mismatch -> human-review. Line-item count mismatch -> human-review.
Concept: evaluation
05

PreToolUse Cap and Duplicate Hook

deterministic policy gate before approve_payment

Sits between the model's tool_use for approve_payment and actual execution. Reads tool_input.vendor_id, tool_input.amount, tool_input.invoice_number. Three checks. (1) Cap: vendor_ytd_spend + amount <= vendor_authorization_cap. (2) Blocklist: vendor not in the active blocklist. (3) Duplicate: no row in the audit log with the same (vendor_id, invoice_number) in the last 90 days. Any check fails and the hook exits 2 with a structured stderr message; the agent observes the deny as tool_result is_error: true and routes to a structured exception block for the AP analyst.

Configurationmatcher: 'approve_payment'. Hook exits 2 with stderr { reason: 'cap_exceeded' | 'vendor_blocklisted' | 'duplicate_detected', detail: ..., recommended_action: ... }. SDK forwards stderr to the model as a tool_result with is_error: true.
Concept: hooks
02 · Problem framing

The problem

What the user needs
  1. Schema-conformant extraction on every invoice: vendor, number, line items, total, currency, due date, PO reference. No prose wrapping; downstream systems must parse cleanly.
  2. Three-way match before approval: invoice, purchase order, goods receipt all agree on amount, vendor, and quantities.
  3. Cap-policy enforcement that cannot be bypassed by clever invoice phrasing: vendor authorization caps, duplicate detection, blocklisted-vendor checks.
  4. Audit-grade trail of every approval and rejection so finance can replay any decision in a quarterly close.
Why naive approaches fail
  1. Prompt 'output JSON' for invoice extraction: ~15% leakage on edge invoices (handwritten notes, mixed languages, credit memos, rotated scans).
  2. Single-pass extraction with no semantic validation: line totals do not match the header; corrupted records ship downstream.
  3. No three-way match: the agent approves an invoice for goods that were never received, or against a PO that does not exist.
  4. Cap policy in the system prompt: ~3% of approvals exceed authorization cap because prompts leak under unusual phrasing.
  5. No duplicate-invoice check: the same invoice number gets paid twice when the vendor re-sends after a delivery confirmation.
Definition of done
  • Forced tool_choice: { type: 'tool', name: 'extract_invoice' } on every extraction call.
  • JSON schema requires vendor_id, invoice_number, line_items[], total_amount, currency (ISO 4217 enum), due_date (ISO 8601), PO_reference (nullable).
  • Validation-retry loop confirms sum(line_items) total, currency in enum, due_date >= invoice_date.
  • Three-way match service reconciles invoice + PO + GRN; variance > 2% routes to human review.
  • PreToolUse hook on approve_payment: deny on cap exceeded, vendor blocklisted, or duplicate (vendor_id, invoice_number) in the last 90 days.
  • PostToolUse audit log writes every approval / rejection / hook decision.
03 · Data flow

One run, traced end to end

Invoice Processing Agent sequence diagram.
Invoice Processing Agent end-to-end flow.
04 · Build

8 steps to production

01

Author the invoice JSON schema as a tool definition

Define the output shape in tools[0].input_schema. Every required field listed in required[]. Currency is an enum that includes an 'unclear' escape hatch. PO_reference is ['string', 'null'] because cash invoices and credit memos have no PO. Every numeric field has minimum: 0. Line items are an array with description, quantity, unit_price, total. The schema is the contract; everything downstream depends on it being right.

Concept: structured-outputs
Python
from anthropic import Anthropic
client = Anthropic()

EXTRACT_INVOICE_TOOL = {
    "name": "extract_invoice",
    "description": "Extract a structured invoice record from a PDF or image.",
    "input_schema": {
        "type": "object",
        "properties": {
            "vendor_id": {"type": "string"},
            "invoice_number": {"type": "string"},
            "invoice_date": {"type": "string", "format": "date"},
            "due_date": {"type": "string", "format": "date"},
            "currency": {
                "type": "string",
                "enum": ["USD", "EUR", "GBP", "INR", "JPY", "unclear"],
            },
            "total_amount": {"type": "number", "minimum": 0},
            "tax_amount": {"type": ["number", "null"], "minimum": 0},
            "PO_reference": {"type": ["string", "null"]},
            "line_items": {
                "type": "array",
                "minItems": 1,
                "items": {
                    "type": "object",
                    "properties": {
                        "description": {"type": "string"},
                        "quantity": {"type": "number", "minimum": 0},
                        "unit_price": {"type": "number", "minimum": 0},
                        "total": {"type": "number", "minimum": 0},
                    },
                    "required": ["description", "quantity", "unit_price", "total"],
                },
            },
        },
        "required": [
            "vendor_id", "invoice_number", "invoice_date", "due_date",
            "currency", "total_amount", "line_items",
        ],
    },
}
02

Force tool_choice and run extraction with vision input

Set tool_choice: { type: 'tool', name: 'extract_invoice' } so the model has no choice but to fire the tool with arguments matching the schema. Pass the invoice as a vision input (PDF page rasterized to image, or direct image upload). The model emits a structured tool_use; the harness extracts tool_use.input as the candidate record.

Concept: tool-choice
03

Wrap extraction in a validation-retry loop

Schema guarantees structure; semantics need code. After parsing, validate: sum(line_items[].total) equals total_amount within 0.01 tolerance; currency in the enum; due_date format and >= invoice_date. On failure, feed the specific error back via tool_result with is_error: true so the model sees what was wrong; retry up to 3 times. Most failures converge in 1-2 retries because the model now knows what the validator rejected.

Concept: evaluation
04

Run a three-way match against PO and goods receipt

Query the PO master by PO_reference and the goods-receipt ledger by the same key. Compare amount (variance <= 2% OK for FX rounding and minor price changes), vendor identity (normalized fuzzy match on vendor name), and line-item count (must match exactly). Variance above any threshold routes to human review with a structured exception block; otherwise auto-proceed.

Concept: evaluation
05

Wire the PreToolUse cap and duplicate-detection hook

Hook on approve_payment. Three checks. (1) Cap: vendor_ytd_spend + amount <= vendor_authorization_cap. (2) Blocklist: vendor not on the active blocklist. (3) Duplicate: no audit-log row with the same (vendor_id, invoice_number) in the last 90 days. Any check fails and the hook exits 2 with a structured stderr message; the agent observes the deny and routes to an exception block for the AP analyst. Deterministic, no prompt-injection bypass.

Concept: hooks
06

Cache the schema and the vendor master

The schema is the largest stable token cost (~1500 tokens for invoice extraction). The vendor master (caps, blocklist, name normalization rules) is also stable per session. Mark both with cache_control: ephemeral so a 5-minute TTL keeps them warm across sustained AP traffic. Realistic savings: ~80% on cached portions, ~50% reduction on overall steady-state cost.

Concept: prompt-caching
07

Use Batch API for overnight bulk runs

Sync API for inbox-arrival latency. For nightly backfills (10K invoices), the Batch API gives a flat 50% discount with a 24-hour SLA. Combined with schema and vendor-master caching (per-100-item sub-batches keep ephemeral cache warm), bulk extraction cost drops ~75% versus naive sync. Resubmit failures the next morning as a fresh batch with the specific error in the next message.

Concept: batch-api
08

Audit-log every approval, rejection, and hook decision

PostToolUse hook on every approve_payment call. Append a row to durable storage: timestamp, vendor_id, invoice_number, amount, currency, three-way-match outcome, hook decisions (cap, blocklist, duplicate), final routing (approved | human-review | denied). Retain at least 7 years for audit compliance. The audit log is the replay tool when finance asks 'why did we approve this in May?' three months later.

Concept: evaluation
05 · Right call, wrong call

9 decisions the exam turns into distractors

01 · Output shape guarantee on extraction
Looks right

Prompt 'output JSON' or 'respond with valid JSON only'

Actually right

Forced tool_choice with input_schema as the contract

DEC-01
02 · Vendor authorization cap enforcement
Looks right

System prompt: 'never approve above the vendor cap'

Actually right

PreToolUse hook reads vendor_ytd_spend, exits 2 on violation

DEC-02
03 · Same invoice arriving twice (vendor re-sends after delivery)
Looks right

Trust the model to notice duplicates in conversation context

Actually right

PreToolUse duplicate-detection hook keyed on (vendor_id, invoice_number) over last 90 days

DEC-03
04 · Bulk overnight processing of 10K invoices
Looks right

Sync API in a tight loop or sync API without caching

Actually right

Batch API + schema and vendor-master caching

DEC-04
05 · Prompt-only field extraction
Looks right

Prompt 'extract this invoice as JSON' leaks ~15% on edge invoices. Downstream parser breaks every seventh document; AP analyst spends the morning re-keying invoices the agent botched.

Actually right

Forced tool_choice: { type: 'tool', name: 'extract_invoice' } plus a strict JSON schema in tools[0].input_schema. The model has no choice but to fire the tool with arguments matching the schema. 100% structural adherence.

AP-INV-01
06 · No semantic validation
Looks right

Single-pass extraction with no math check. The model returns a structurally-valid record where line totals sum to 4950 but the header total says 5000. Bad data ships downstream; quarterly close finds the discrepancy three months later.

Actually right

Validation-retry loop. After parse, validate sum(line_items[].total) total_amount (within 0.01 tolerance), currency in ISO 4217 enum, due_date >= invoice_date. On failure, feed the specific error back; retry up to 3 times; route to human review if still failing.

AP-INV-02
07 · No three-way match
Looks right

Agent approves an invoice that has no matching purchase order, or where the goods receipt was for fewer items, or where the vendor name on the invoice does not match the vendor on the PO. AP pays for goods never received, or pays the wrong vendor.

Actually right

Three-way match service queries PO master and goods-receipt ledger. Compares amount (variance <= 2% OK), normalized vendor name, line-item count. Variance above thresholds routes to human review with a structured exception block.

AP-INV-03
08 · Cap policy in the system prompt
Looks right

System prompt: 'never approve more than the vendor authorization cap'. Production logs show ~3% of approvals exceed the cap because the prompt language leaks under unusual phrasing or when the agent is processing many invoices in one session.

Actually right

PreToolUse hook on approve_payment reads tool_input.vendor_id and tool_input.amount, queries the vendor master for vendor_ytd_spend + cap, exits 2 on violation with a structured message including cap_remaining. Deterministic, not probabilistic.

AP-INV-04
09 · No duplicate-invoice check
Looks right

Vendor re-sends the same invoice number after delivery confirmation, or the same invoice is uploaded twice through different channels (email + portal). The agent approves both. AP discovers the duplicate payment in next month's reconciliation.

Actually right

PreToolUse hook queries the audit log for any row with the same (vendor_id, invoice_number) in the last 90 days. On match, exits 2 with the prior approval date. Stateless, auditable, prevents race conditions in parallel runs.

AP-INV-05
Invoice Processing Agent failure map.
06 · Budget

Cost & latency

~$0.001 to $0.003Per-invoice synchronous extraction (cached schema)

Schema ~1500 tokens at cache-read price plus image vision tokens (~1000-2000) plus ~150 output. Sustained AP traffic with cache hits >= 70% drops effective cost predictably.

~$0 token cost; ~10-30 ms latencyThree-way match service

Pure SQL queries against PO master and goods-receipt ledger. No LLM call. Latency is dominated by the database round-trip.

~$0; ~5 ms latencyPreToolUse hook overhead

Subprocess reads stdin JSON, runs three SQL queries (vendor cap, blocklist, duplicate), exits 0 or 2. No LLM call. Latency below the noise floor of any tool dispatch.

~75% off naive syncBatch overnight (10K invoices, batch + caching)

Batch API flat 50% discount times schema and vendor-master cache (~80% off cached portion). 10K invoices at typical complexity drop from ~$30 sync uncached to ~$8 batch cached.

~+25% on records that retryValidation-retry overhead

5-10% of records retry once; 1-2% retry twice. Specific-error feedback converges quickly. Pipeline cost up ~5% to gain ~99% schema-conformance plus ~99% semantic-conformance.

~$1.00 to $3.00Per-1000-invoices total (steady state)

Sync cached extraction at scale. Adding human review of unconverged records adds operator-time cost but recovers the long tail of edge invoices.

07 · Ship checklist

Check every gate before release

0/11 checked
  • structured-outputs
  • tool-choice
  • structured-outputs
  • evaluation
  • evaluation
  • hooks
  • prompt-caching
  • batch-api
08 · Practice

5 exam-pattern questions

Work through one question at a time, check the architecture, then move through the set.

Question 1 of 5 · D2Choose the best answer

An invoice extraction agent uses prompt-only extraction. Roughly 15 percent of records arrive with prose preambles ('Sure, here is the JSON:') and the parser breaks. What is the architectural fix?

09 · FAQ

Frequently asked

How do you handle handwritten or scanned invoices with poor image quality?

Vision-capable extraction handles most cases. For edge invoices (rotated scans, faded ink, handwritten amendments), the validation-retry loop catches arithmetic mismatches and the three-way match catches structural issues. Records that fail after 3 retries route to human review with the original image attached. Stratified accuracy reporting by document-type quickly surfaces vendors whose invoices need a layout-aware preprocessing step.

What happens if a vendor has multiple naming variations (Apple Inc, APPLE, Apple, Inc.)?

The vendor master holds the canonical vendor_id and a list of name variations. The extraction schema requires the model to extract the vendor as text; a normalization step (lowercase, strip punctuation, fuzzy match against the vendor master) resolves it to a vendor_id. The duplicate-detection hook keys on vendor_id, not the raw name, so naming variation does not break uniqueness.

Can the agent process multi-currency invoices in one workflow?

Yes. The schema enforces currency as an ISO 4217 enum. The cap policy and duplicate detection key on vendor_id and amount in the invoice currency; the cap can be denominated per-vendor in the vendor master. For consolidated reporting, a daily FX-rate table converts to a base currency at audit-log write time.

How do you handle credit memos (negative invoices)?

Credit memos use the same schema with total_amount representing the credit (positive number) and a separate document_type enum field that distinguishes invoice from credit_memo. The PreToolUse hook treats credit memos as vendor_ytd_spend - amount (effectively decreasing YTD spend). Three-way match runs against the original invoice and the credit-memo reason code instead of a PO and GRN.

Should the agent auto-approve, or always route to human review?

Auto-approve only when all gates pass: schema valid, semantic validation passed, three-way match within thresholds, PreToolUse hook approved (cap, blocklist, duplicate). Any failure routes to human review with a structured exception block. Auto-approval rate at steady state is typically 75-85%; the remaining 15-25% needs an analyst's eye. The point of the agent is not to remove the analyst; it is to make the analyst's queue much smaller and every queued invoice well-explained.

How long do you retain the audit log?

At least 7 years for financial-record compliance (US SOX, EU equivalent). Append-only schema; immutable rows; indexed by vendor_id, invoice_number, and date. Replay tool reconstructs any approval decision in seconds when finance asks 'why did we approve this in May?' three months later.

Is Batch API worth using for fewer than 1000 invoices a night?

Sometimes. Batch API gives a flat 50% discount but with a 24-hour SLA. For under 500 invoices, the Batch overhead and the latency may not be worth it; sync extraction is cheaper end-to-end when AP needs same-day processing. For nightly backfills of historical invoices or large vendor consolidations (more than 1000 documents), Batch API earns its keep.

Help someone build it

Share this scenario.

One share is one less team repeating the same architecture mistake.