Krishna Bhupathi

Fig. 3

Docwise

AI extraction of supplier invoices, with every value audited.

A 90-second walkthrough with narration and captions. Sound plays only when you press play.

The problem

Small finance teams type supplier invoices into the books by hand. AI extraction is faster, but a wrong total that looks certain is worse than no help. Docwise reads the invoice, checks every value the way an auditor would, and approves only what it can vouch for. Everything else waits for a person, with the reason shown.

For ops and finance teams that process supplier invoices, and for developers who want the same thing as an API.

The product

The Docwise review page: the original invoice on a dark bench on the left, and the audit sheet on the right with the subtotal, tax and total highlighted because they don’t add up.
An invoice whose total is wrong on purpose. The failed cross-foot mark highlights the three values, and a person decides.
The Docwise home page: the headline, a specimen audit sheet with a highlighted total, and sample invoices from the test set.
The home page. Sample invoices from the test set open with no sign-up and no model call.

Hard problems

  • Confidence that means something

    Models are poor judges of their own certainty, so Docwise doesn’t ask for a score. A value is high confidence only if no rule implicates it, the model didn’t list it as uncertain, and, for a PDF with a text layer, the value is actually printed in the document. Numbers and dates match by value, so 1.234,56 € matches 1234.56. Anything else goes to a person, with the reason beside the value.

  • The evaluation is part of the product

    Sixty invoices are generated from a seed, each with its true values: five layouts with their local number and date formats, scans, phone photos, two-page invoices, handwritten notes, and three that don’t add up on purpose. A third were held out and scored once, after tuning ended. The eval runs the app’s own functions, and CI re-scores every saved model answer on every push, so a change that moves a reported number fails the build.

  • A model that never writes to the database

    Claude answers through structured outputs, and Zod parses every answer. A malformed one goes back once with the exact errors; a second failure is reported, never stored. A reviewer’s corrections are kept beside the model’s values, so the log can always tell what the model said from what a person fixed.

Measured

20 / 201
Held-out invoices fully correct, scored once
01
Escaped errors: wrong values marked high confidence
$0.01151
Median model cost per invoice, at list prices
155 + 82
Unit and database tests, plus browser tests

Evaluation

20 held-out invoices, prompt v1 on Claude Sonnet 5.5, scored once.1
Header fields correct100%
Line-item F1100%
Invoices fully correct20 of 20
Sent to a person1 of 20, the invoice that doesn’t add up
Escaped errors0
Model cost per invoice, median / p95$0.0115 / $0.0170
Time per invoice, median / p953.1 s / 5.6 s

One prompt change, before and after

The prompt tells the model how to write dates, amounts and currency codes. Without those rules, on the 40 tuning invoices, Claude still got every value right, but copied dates and amounts as printed. Every first answer failed validation and needed the retry.

MeasureWithout the rules3With the rules4
Model calls per invoice21
Median cost$0.0251$0.0111
Median time6.3 s3.1 s
Sent to a person12.5%5%

Twenty invoices is a small sample: no escaped errors means none occurred here, not that the rate is zero. The invoices are generated, so real ones, with odd layouts, stamps and poor scans, will be harder.

Every run, with each answer the model gave, is in docs/measurements.

How it’s built

  • Next.js 16 (App Router, Server Actions)
  • TypeScript
  • Claude Sonnet 5.5 with structured outputs, PDF and image input
  • Zod
  • Postgres on Neon, Drizzle ORM
  • Cloudflare R2, private with signed URLs
  • Vitest with PGlite, Playwright
  • Vercel, GitHub Actions
  1. Browser → R2Sends the invoice straight to a private bucket through a signed URL.
  2. AppChecks the stored file’s type, size and pages, and queues it. An API client sends the file with a Bearer key instead.
  3. App → ClaudeIn the background, sends the PDF or image to Claude Sonnet 5.5, which answers in a strict JSON schema.
  4. AppZod parses the answer. A malformed one goes back once with the exact errors.
  5. AppRules check the dates and the arithmetic. Each field’s confidence comes from the rules, the model’s own doubt and the PDF’s text.
  6. App → PostgresStores only validated values. Every field high: approved. Anything low: waits for review.
  7. Reviewer → AppCorrects values beside the original. Corrections are stored next to the model’s values, never over them.
  8. App → CSV, APIApproved invoices go out as CSV, one row per line item, or as JSON.
  9. npm run evalRuns the same functions on the test set. CI re-scores the saved answers on every push.

More decisions and their trade-offs are in the README.

Replies within a working day.

Sources

  1. Docwise held-out split: 20 generated invoices, scored once after tuning ended, with prompt v1 on Claude Sonnet 5.5. Header fields 100% correct, line-item F1 100%, 20 of 20 fully correct, review rate 5% (1 of 20), 0 escaped errors. Model cost per invoice at list prices: median $0.0115, p95 $0.0170. Time per invoice: median 3.1 s, p95 5.6 s. Source: docwise-extract/docs/measurements.
  2. Docwise tests: 155 Vitest tests (unit, database tests on PGlite, and a replay of every saved eval run) and 8 Playwright tests on a production build, green in CI. Source: docwise-extract PROGRESS.
  3. Docwise tuning split, 40 invoices, the bare prompt (v1 without its rules for values) on Sonnet 5.5: 40 of 40 fully correct after the retry, 2 model calls per invoice, median cost $0.0251, median time 6.3 s, review rate 12.5%. Calls per invoice are in the JSON record. Source: eval-03-bare-sonnet-5-5-tuning.json.
  4. Docwise tuning split, 40 invoices, prompt v1 on Sonnet 5.5: 40 of 40 fully correct, 1 model call per invoice, median cost $0.0111, median time 3.1 s, review rate 5%. Calls per invoice are in the JSON record. Source: eval-02-v1-sonnet-5-5-tuning.json.