How it works
One supplier document, from the PDF landing in an inbox to the record your system reads. It's the demo built into the app, so you can run it yourself.
01Intake
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.
- EmailSuppliers email an intake address as they always have. The PDF comes in on its own, and the sender's address tells Linekeeper which supplier it is. Nothing changes on their end.
Every file is stored under its SHA-256 hash, so a document sent twice is stored once. The original PDF is always kept, not just the reading of it.
02Extraction
The PDF goes to a model that has to return a fixed set of fields, or the read fails. No free text gets parsed after the fact.
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 fieldEach line, and the document as a whole, gets a score. Under 0.5 on a line, or 0.6 overall, raises a flag even when the numbers match.
It gets better per supplier
Notes you keep on a supplier, like where their PO number sits or whether prices are ex-GST, go into every read of that supplier's documents. So do the last few readings someone corrected and confirmed, as worked examples. A layout you see every week gets steadily more reliable.
03Matching
First it finds the order: an open one whose PO number matches, ignoring punctuation and case, so PO-10432, po 10432 and PO10432 are the same. If two orders share a number, the supplier decides. With no readable PO number, it tries supplier plus grand total within 1%, and only 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 document row is paired with an order row by SKU first. Suppliers often use their own codes, so when those differ it compares descriptions and needs 60% of the words to agree. Each order line can be claimed once, so a repeated SKU shows up as an extra row instead of quietly overwriting the first.
Quantity has to match exactly. Price gets a tolerance you set, the larger of a fixed amount and a percentage, so rounding noise doesn't page anyone at 6am.
An invoice covering several POs is matched against each order separately. A short delivery isn't wrong, just incomplete: the order stays open for what's still owed, the next document is checked against that balance, and the order closes once everything has arrived. Counting the delivery when it lands does the same for what was really in the box.
04The 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 the total of your order lines with the document's subtotal, not its grand total, because tax and freight aren't on your order.
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 off by the missing shim line and the gasket overcharge, and the gasket price came back at 0.41 confidence because it was written by hand.
Two of four lines need a person. The other two are already done.
What it catches, and how it looks.
The same view every time: what you ordered on the left, what arrived on the right, and a note against anything that does not agree. Three different weeks, three different problems.
A price that quietly moved
The most common one, and the easiest to pay without noticing.
A short shipment nobody mentioned
Every price correct, three of four quantities cut. Nothing on the document says so.
A substitution and a charge you never agreed
The hardest to spot by eye, because the paperwork looks complete.
In all three, the lines that matched went through untouched. Only the marked rows needed anyone.
05Confirm, correct, and hand off
Every field on the review screen is editable. If the reader got a handwritten price wrong, 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 reading becomes a worked example for that supplier's next document. The 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.