How it works
One supplier document, followed end to end: from the PDF landing in an inbox to the record your system reads. The example below is the one built into the app, so you can run it yourself.
Step 01
Intake
Two ways in, both ending at the same place.
- UploadDrag a PDF onto the app, or pick it from disk. Useful when you already have the file, or you're working through a backlog.
- EmailA Cloudflare Email Worker watches an intake address. The supplier replies as they always have, the worker parses the MIME, pulls the PDF attachment, and posts it in with the sender address as a supplier hint. Nothing changes on their end.
Every file is hashed with SHA-256 on arrival and stored under that hash, so the same document sent twice is the same object. The raw PDF is kept: the extraction is never the only copy.
Step 02
Extraction
The PDF goes to a model with a fixed output schema: not free-form text that gets parsed afterwards. It has to return the fields in that shape or the call fails.
Header: document type, PO number, supplier, confirmation number, order date, promised ship date, currency, subtotal, tax, shipping, grand total. Then a row per line: line number, SKU, description, quantity, unit price, line total, unit of measure, notes.
Two rules that matter more than accuracy
- Empty beats wrongA field it can't read comes back null. A guessed price is worse than a blank one, because a blank one gets looked at.
- Confidence per fieldEvery line carries its own score, and the document carries an overall score. Anything under 0.5 on a line, or 0.6 overall, raises a flag on its own, even when the numbers happen to match.
It gets better per supplier
You can store notes against a supplier: where the PO number sits on their layout, whether prices are ex-GST, which column holds their line codes. Those notes go into the prompt for every document from that supplier. So do the last few extractions a person corrected and confirmed, as worked examples. A layout you see every week gets steadily more reliable instead of failing the same way each time.
Step 03
Matching
Finding the order comes first. Linekeeper looks for an open order whose PO number matches once punctuation and case are stripped: PO-10432, po 10432, and PO10432 are the same key. If several orders share that number, the supplier name breaks the tie. If the document has no readable PO number at all, it falls back to supplier plus grand total within one percent, and only accepts that when exactly one order fits.
No order found is not an error: it's the PO_UNMATCHED flag, and every line comes through as unexpected so a person can point it at the right order.
Then line by line
Each row on the document is matched to a row on the order by normalised SKU first. When the codes don't agree, and they often don't, because suppliers use their own, it falls back to token overlap on the description, needing 60% agreement before it will pair two lines. Each order line can only be claimed once, so a document that repeats a SKU produces an extra unexpected row rather than silently overwriting.
Quantity is compared exactly. Price is compared against a tolerance, the larger of an absolute amount and a percentage, both of which you set, so rounding at the fourth decimal place doesn't page anyone at 6am.
Step 04
The flags
Per line, one of five verdicts:
- OKMatched an order line, quantity exact, price inside tolerance. Never needs a person.
- QTY_MISMATCHMatched, but the quantity differs. Usually a partial allocation or a pack-size assumption.
- PRICE_MISMATCHMatched, but the unit price is outside tolerance. The delta and the expected price are both carried through.
- MISSING_IN_DOCOn your order, absent from their document. They dropped a line, or split the shipment without saying so.
- UNEXPECTED_IN_DOCOn their document, not on your order. A substitution, a freight line, or the wrong PO.
And at the header, four more that apply to the document as a whole:
TOTAL_MISMATCH compares your order's line sum against the document's subtotal, not its grand total: tax and freight aren't on your order lines, so comparing against the grand total would flag every document ever sent.
A full document
What the review screen shows.
Four ordered lines, four verdicts. This is the demo document in the app, matched against order PO-10432 from Kaimai Components Ltd.
| Verdict | SKU | Description | Qty | Unit | Expected |
|---|---|---|---|---|---|
| OK | BRK-14A | Bracket, 14mm, zinc | 40 | 6.50 | - |
| PRICE_MISMATCH | GSK-22 | Gasket set, viton | 12 | 39.00 | 35.00 |
| OK | PLT-09 | Cover plate, powder-coated | 8 | 69.50 | - |
| MISSING_IN_DOC | SHM-03 | Shim, 0.5mm stainless | - | - | 100 @ 0.80 |
Header flags on this document: TOTAL_MISMATCH LOW_CONFIDENCE_LINE: the subtotal is short by the missing shim line plus the gasket overcharge, and the gasket price came back at 0.41 confidence because it was handwritten on the original.
Two of four lines need a person. The other two are already done.
Step 05
Confirm, correct, and hand off
Every field on the review screen is editable: header and line both. When the reader got the handwritten price wrong, you fix it in place and confirm. Three things happen:
- Re-matchYour corrected values go back through matching, so the flags reflect what's actually true, not what the first read thought.
- RememberThe corrected extraction is stored as a worked example for that supplier and goes into the prompt next time. Five most recent are kept.
- Hand offA normalised record is written and made available at a stable URL. If you've set a webhook, it's posted there in the same request.
Rejecting works too: the document is marked and nothing is emitted, but the file and the extraction stay for the record.
Limits
What it doesn't do yet.
- One order per documentNo partial shipments, multi-PO documents, or back-order splits.
- Single workspaceNo multi-tenant accounts or per-user roles.