CLI · commit 039b38b

receiptsnap

Receipt photos to structured expenses

Download receiptsnap.zip
Quickstart
unzip receiptsnap.zip && cd receiptsnap
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
.venv/bin/python -m pytest -q

receiptsnap

Point it at receipt photos or PDFs and get validated expenses in a CSV, a JSON file or a ledger-style journal. One call to Claude Haiku 5.5 reads each receipt (merchant, date, line items, GST/VAT breakdown, tip, total, payment method); everything else (cleanup, duplicate detection, arithmetic checks, categorisation, export) is deterministic Python, so it stays cheap and you can trust what lands in your books. Anything that does not add up goes to a review queue instead of your ledger.

Features

Install

uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
source .venv/bin/activate
export ANTHROPIC_API_KEY=sk-ant-...     # not needed for --dry-run or --fixtures

Quickstart (no API key needed)

receiptsnap scan examples/receipts --fixtures examples/receipts \
    --rules examples/rules.toml --out expenses.csv --journal main.journal

--fixtures DIR reads the recorded model response <image stem>.json instead of calling the API. With a real key, drop --fixtures.

Usage

Dry run

Shows exactly what would be sent, with an input token estimate, and touches nothing.

$ receiptsnap scan examples/receipts/fuel.png --dry-run
examples/receipts/fuel.png dry-run
image 636x480 (30 KiB jpeg), ~547 input tokens
model: claude-haiku-5-5
system: You extract structured data from a single receipt or bill image. Copy numbers exactly as
printed; never invent line items. ... confidence (0 to 1) reflects how legible and complete the receipt is.
user: [image] Extract this receipt.

Dry run: nothing was sent. Estimated input tokens: 547

Scan

$ receiptsnap scan examples/receipts --fixtures examples/receipts --rules examples/rules.toml \
      --out expenses.csv --journal main.journal --verbose
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
┃ File               ┃ Status    ┃ Merchant           ┃ Date       ┃        Total ┃ Account            ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
│ cafe_usd.png       │ added     │ Blue Door Cafe     │ 2026-10-12 │    28.14 USD │ Expenses:Food:Din… │
│ fuel.png           │ added     │ Bharat Petroleum   │ 2026-10-09 │ 3,382.19 INR │ Expenses:Transpor… │
│ grocery.png        │ added     │ Green Basket       │ 2026-10-07 │ 1,193.50 INR │ Expenses:Food:Gro… │
│                    │           │ Supermart          │            │              │                    │
│ grocery_retake.jpg │ duplicate │ looks like         │            │              │                    │
│                    │           │ grocery.png        │            │              │                    │
│ parking_flagged.p… │ review    │ City Centre        │ 2026-10-14 │   267.80 INR │ Expenses:Transpor… │
│ restaurant_gst.png │ added     │ Spice Route        │ 2026-10-03 │ 1,260.00 INR │ Expenses:Food:Din… │
└────────────────────┴───────────┴────────────────────┴────────────┴──────────────┴────────────────────┘
needs review parking_flagged.png: subtotal + taxes + tip = 247.80 does not match total 267.80
tokens in=0 out=0 estimated cost=$0.000000 (5 cached/fixture)
4 added, 1 review, 1 duplicate

Running the same command again reports 4 exists and adds no rows. The output is chosen by --out (.csv or .json; default expenses.csv when neither option is given) and --journal; both may be used together.

The journal (main.journal) is balanced double-entry text:

2026-10-03 Spice Route Kitchen  ; id: a720da98d6b9
    Expenses:Food:Dining  1,200.00 INR
    Expenses:Taxes:CGST   30.00 INR
    Expenses:Taxes:SGST   30.00 INR
    Assets:Bank:HDFC

Check it with ledgerline: cat examples/accounts.journal main.journal > all.journal && ledgerline check -f all.journal prints OK: 5 transactions, 15 postings.

Review

Flagged receipts wait in .receiptsnap/queue.json together with the outputs they were destined for. receiptsnap review shows each one with its problems; choose accept, edit (merchant, date, currency, subtotal, tip, total, account; re-validated afterwards), skip (stays queued) or drop. Accepted rows go to the original --out/--journal.

$ receiptsnap review
   parking_flagged.png  [5d6a84db0695]
┌──────────┬────────────────────────────┐
│ merchant │ City Centre Parking        │
│ total    │ 267.8                      │
│ ...      │ ...                        │
└──────────┴────────────────────────────┘
  ! subtotal + taxes + tip = 247.80 does not match total 267.80
[a]ccept [e]dit [s]kip [d]rop [a/e/s/d] (e): ... total (267.8): 247.80 ...
1 accepted, 0 left in queue

Watch

receiptsnap watch ~/Receipts --journal main.journal --rules rules.toml polls the folder (--interval, default 5 s) and processes new files; --once processes what is there and exits. It accepts the same options as scan.

Rules

[defaults]
payment_account = "Assets:Bank:HDFC"   # when the payment method is unknown
tax_prefix      = "Expenses:Taxes"     # Expenses:Taxes:CGST, ...
tip_account     = "Expenses:Tips"
roundoff_account = "Expenses:RoundOff"

[payment]                               # payment_method -> funding account
cash = "Assets:Cash:Wallet"

[[rule]]                                # first match wins, case-insensitive regex on the merchant
match = "petroleum|shell"
account = "Expenses:Transport:Fuel"

No rule matched: the model's suggested_category is used if it looks like an account, else Expenses:Uncategorized. The accounts named in your rules are passed to the model as preferred suggestions.

Configuration

Option / env Meaning
ANTHROPIC_API_KEY credentials (or ant auth login); never read from a flag
--model / RECEIPTSNAP_MODEL default claude-haiku-5-5
--state-dir / RECEIPTSNAP_HOME cache, seen hashes, review queue; default .receiptsnap
--force skip duplicate detection
--fixtures DIR recorded responses instead of the API

Cost: Haiku 5.5 at $0.10 / 1M input and $0.50 / 1M output tokens; a receipt is roughly 600 input tokens, so about $0.0001 each. --verbose prints the real usage. Responses are cached, so re-runs are free.

Architecture

Module Role
imaging.py load images/PDFs, preprocess, dHash, token estimate
extract.py prompt, Haiku call via messages.parse, cache, fixtures, error mapping
validate.py arithmetic, GST, date and currency checks
rules.py TOML rules, category and payment accounts
writers.py CSV / JSON / journal writers, idempotent by ID
store.py state directory: seen hashes, review queue
pipeline.py per-file flow tying the above together
cli.py scan, watch, review

See docs/design.md for the algorithms.

Tests

.venv/bin/python -m pytest

65 tests, all against a fake client (no network): preprocessing, dedupe, validation maths, rules, writers, idempotency, error mapping, and the CLI end to end. scripts/make_examples.py regenerates the example receipts and recorded responses.

Limitations