Skip to content

Repository files navigation

InvoiceAgent — Self-Correcting Invoice OCR Pipeline in C# (.NET 8, IronOCR + OpenAI GPT-5.5)

.NET IronOCR OpenAI License: reference build

InvoiceAgent is a working C#/.NET reference implementation of a self-correcting document-extraction agent for accounts-payable automation. It OCRs a scanned invoice with IronOCR (Tesseract under the hood), sends the extracted text to an LLM (OpenAI's gpt-5.5 in this build) to structure the fields, and then hands everything to deterministic C# for arithmetic validation, field-level provenance against the original OCR tokens, a two-rung retry ladder (alternate OCR character candidates, then a targeted crop-and-reread at higher DPI), and escalation to a human reviewer when the numbers still don't add up.

It's built as a companion codebase to the article Building a Self-Correcting Document Extraction Agent in C# with IronOCR and Claude, turning that article's code samples into a compiling, testable, actually-runnable .NET solution — verified against a real IronOCR + real LLM run on a real (synthetically degraded) scanned invoice, not mocked.

Use this repo if you're evaluating: invoice OCR in C#/.NET, IronOCR + LLM extraction pipelines, OCR confidence scoring and field provenance, an arithmetic-validation retry ladder for noisy OCR, or how to wire Tesseract-based OCR output into an LLM structured-extraction call for AP / invoice-processing automation.

Highlights

  • Real failure→recovery case, not staged: a genuine OCR misread (£412.50£472.50, 58.9% confidence) caught by arithmetic validation, run through both retry rungs, and escalated with full provenance — see artifacts/run-log.txt and artifacts/failure_before.png.
  • Field-level provenance: every extracted value is traced back to the exact OCR token(s) it came from, with that token's real confidence score and bounding box — never a model-invented confidence.
  • A cost-ordered retry ladder: cheapest fix first (Tesseract's own alternate character candidates, no re-scan), then a targeted crop-and-reread at higher DPI, then escalation — never a blind retry loop.
  • 3 real SDK/API drifts documented, found by actually compiling against current package versions rather than trusting the article's pinned versions — see "What changed vs. the article's code" below.
  • 2 real bugs found only by running it for real (a DPI mismatch between OCR passes, a crop-reread that needs to fail gracefully) — see the "Run the pipeline for real" commit.

LLM provider: OpenAI, not Claude — read this before reusing anything from Extraction/

The article this repo is built from is specifically about Claude/Anthropic. The schema prompt, the Temperature-deprecation finding, the model-ID comments — all of it was written and verified against the Anthropic C# SDK. This repo's src/InvoiceAgent.Core/Extraction/OpenAiExtraction.cs calls OpenAI's Chat Completions API instead, using the official OpenAI NuGet package and model gpt-5.5.

This was a deliberate, explicit choice made mid-build, not a silent substitution: no working ANTHROPIC_API_KEY was available in this environment (only OPENAI_API_KEY, and the first value found there was itself invalid — a fresh key had to be issued), and the user chose to switch providers rather than wait on Anthropic credentials. If you want this repo to actually match the article, get an Anthropic API key and either restore ClaudeExtraction.cs from git history (see the "Scaffold" commit) or write an equivalent against the current Anthropic SDK using the API notes below.

Everything else in this repo — OCR, provenance mapping, validation, the retry ladder — is provider-agnostic and unaffected by this swap; only Extraction/ and the OpenAI package reference in InvoiceAgent.Core.csproj are OpenAI-specific.

Screenshots

Reviewer screen — what a human sees when the pipeline escalates. Real bounding box, real OCR confidence, real escalation summary, generated from an actual run against test-assets/invoice_scan.audit.json:

Reviewer screen showing the escalated invoice with the disputed field boxed

The disputed field, before escalation — IronOCR read £472.50 at 58.9% confidence where the printed amount is £412.50:

Cropped invoice region showing the OCR misread that failed arithmetic validation

Table of contents

Pinned versions (as actually installed, 2026-08-19)

The article's "Tested environment" note pins IronOcr 2025.11 and Anthropic 12.9.0. IronOcr was long superseded on NuGet by the time this repo was built, so the newest available version was used instead, per the task's own instruction to do that and report the difference. Anthropic was dropped entirely — see above.

Package Article pin Actually installed Notes
IronOcr 2025.11 2026.8.1
IronPdf (unpinned) 2026.8.1
OpenAI — (article uses Anthropic) 2.13.0 Provider swap — see above
SixLabors.ImageSharp 4.1.0 Added for Phase 2's scan-degradation step (ImageMagick was not available in this environment)

Target framework: net8.0, as the article specifies — but this machine only has the .NET 10 SDK/runtime installed, no .NET 8 runtime. Every executable project (InvoiceAgent.Cli, InvoiceAgent.Tests) sets <RollForward>LatestMajor</RollForward> so the net8.0-targeted apphost/testhost launches against the installed net10 runtime instead of failing with "You must install or update .NET to run this application." Remove that property once a real net8.0 runtime is installed, if you want a strict match.

What changed vs. the article's code, and why

Verified by actually restoring these package versions and compiling against them — not assumed from the article's text.

Anthropic SDK, found before the provider swap (kept here as a real, verified finding even though this repo no longer uses the SDK — worth knowing if you restore ClaudeExtraction.cs):

  1. MessageCreateParams.Temperature is [Obsolete] in Anthropic SDK 12.40.0: "Models released after Claude Opus 4.6 do not support setting temperature." Since claude-sonnet-5 postdates that cutoff, the article's Temperature = 0 is not just unnecessary, it risks a 400.
  2. No TextContent type exists in that SDK version's Anthropic.Models.Messages namespace. Message.Content is IReadOnlyList<ContentBlock>, where ContentBlock is a discriminated union (TryPickText, TryPickToolUse, ...), not a class hierarchy you can .OfType<T>() against — use block.TryPickText(out var textBlock) instead.

IronOcr, still live in this repo:

  1. OcrResult.Character.Choices is Choice[] (a plain array) in IronOcr 2026.8.1, not a type exposing an instance .Count property — .Count resolved to the LINQ extension method group and failed to compile as .Count > 0. Reconciliation.cs uses .Length instead.

Everything else in the article's code blocks (the schema prompt, the provenance sliding-window matcher, the per-line/subtotal/total validation checks, the character-substitution reconciliation logic, the crop-and-reread mechanism, TargetDPI/EnhanceResolution/WhiteListCharacters) compiled as described once ported into this project's namespaces.

Project layout

InvoiceAgent/
├── src/
│   ├── InvoiceAgent.Core/       # OCR, extraction, provenance, validation, escalation — the article's C# logic
│   └── InvoiceAgent.Cli/        # `dotnet run -- invoice.pdf` entry point, wires the full pipeline together
├── tests/InvoiceAgent.Tests/    # xUnit tests for InvoiceValidator against the synthetic Bright Path invoice
├── test-assets/                 # Synthetic test invoice (generated, not the article's real-world data)
└── artifacts/                   # Run logs, cropped before/after images, reviewer-screen mockup

Setup

  1. Get a 30-day trial key from ironsoftware.com (one key validated for both IronOCR and IronPdf in this build — Iron Software normally licenses them separately, so don't assume that holds for your account) and an API key from platform.openai.com, then set:
    IRONOCR_LICENSE_KEY=...
    IRONPDF_LICENSE_KEY=...
    OPENAI_API_KEY=...
    
  2. dotnet build
  3. dotnet test tests/InvoiceAgent.Tests/InvoiceAgent.Tests.csproj

Running the pipeline

dotnet run --project src/InvoiceAgent.Cli -- test-assets/invoice_scan.pdf --verbose

Stages are logged as [LOAD], [OCR], [EXTRACT], [PROVENANCE], [VALIDATE], [RETRY 1], [RETRY 2], [RESULT], matching the article's "Running the complete pipeline" section. --verbose additionally writes a <document>.audit.json file next to the input, in the shape described under "Making it auditable".

Exit code 0 = accepted, 2 = escalated to human review, 1 = a hard failure before validation ran at all (missing file, unparseable extraction).

What's not implemented here

  • MCP layer: the article's "Wiring it to an agent with MCP" section is explicitly conceptual, not working code. See src/InvoiceAgent.Core/Mcp/README.md.
  • Three-way match: the PO/delivery-note matching described in "The three-way match" needs a second, already-structured data source this repo doesn't have. EscalationTrigger.MissingPurchaseOrder exists in the enum for completeness but is never raised.

Related reading


Questions or corrections about this codebase specifically (not the article) are welcome via GitHub issues.

About

Self-correcting invoice OCR pipeline in C#/.NET: IronOCR + OpenAI GPT-5.5, arithmetic validation, a cost-ordered retry ladder, and full field-level OCR provenance for AP automation.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages