Every field is read, checked, and traced back to the original photo.
Not a scanner that hands you a total. A pipeline that decides whether that total is a legally valid GST claim, splits it correctly across tax categories, and posts it to a ledger that cannot go out of balance.
The original is the legal record — kept forever
A quality gate on the device rejects a blurry or non-document frame before it ever uploads — the ATO only accepts a receipt copy that's a true and clear reproduction of the original. Once it's captured, the original bytes are never overwritten or recompressed; a separate normalised copy is derived for the model to read.
Versioned, replayable — never a one-shot read
Claude Haiku 4.5 vision reads every field as {value, confidence, bbox}, and returns null with a reason rather than guessing. Anything uncertain escalates to Sonnet 5. Every run is kept — nothing is overwritten — so a better model in six months can re-run over your whole history without you lifting a finger.
A confident wrong ABN is worse than a missing one — it silently creates an unclaimable GST credit. The model is told to say so.
~85% of scans resolve on the primary model. The remainder escalate to a stronger model, not to a person, unless validators still fail.
Re-running extraction inserts a new run and moves the pointer. A posted transaction is never silently rewritten — it raises a review task instead.
Nine deterministic checks — the biggest accuracy lever
Costs nothing to run and catches more than a bigger model would. Every extraction is checked against arithmetic and the ATO's own rules before anything is trusted, in code, not by asking the model to grade itself.
Mod-89 checksum on every ABN read from the page.
Checked against the ABR register — legal name and GST registration status.
Exclusive amount + GST must equal the inclusive total, to the cent.
An all-taxable invoice must show GST ≈ 1/11 of the inclusive price.
Every line must add up to the invoice total before anything is trusted.
AU 5c cash rounding tolerated to ±$0.02; the difference is recorded, not discarded.
GST-free and taxable subtotals must each reconcile — not just the total.
The seven ATO elements, checked — this is what sets is_tax_invoice.
Not in the future; not more than ten years past.
Any failure escalates to the stronger model. Still failing → flagged needs_review, with the specific check attached — never a silent guess.
Double-entry, and provably balanced
Every scan proposes a transaction with splits that must sum to exactly zero. A DEFERRABLE constraint trigger enforces it at commit, for posted transactions only — drafts can sit half-built and be edited freely. This is not a spreadsheet convention someone has to remember. Postgres refuses the commit.
Mapped the moment a line is posted
Each tax code carries its BAS labels — G11 and 1B for a non-capital purchase claiming a credit, G10 for capital purchases, G1 and 1A for a sale. Quarterly BAS becomes a query grouped by label, not a quarter spent re-reading a shoebox.
Knows what it doesn't know — and says so before lodgement
A validator failure doesn't get silently dropped or silently claimed. It surfaces as a specific, fixable line: which receipt, which check, which fix — an ABN to chase, a re-issued invoice to request.
of GST credits you cannot claim. 12 receipts are missing a supplier ABN.
A grocery receipt, split correctly
Fresh food is GST-free; packaged snacks are taxable. Hubdoc records one header total. Snap Apps keeps a subtotal per tax category, on the same receipt, reconciled independently.
Filled in before 1 July, not after
An occupation-aware deduction worksheet — car, travel, clothing and laundry, tools and equipment — built from a year of scans as they happen, not reconstructed from memory at tax time.
Posts to the chart of accounts you already run
Snap Apps doesn't replace Xero — it feeds it a ledger that's already reconciled. Categories map to your existing accounts; BAS labels travel with each line; nothing here asks you to migrate.