2026-05-26 · Koryu, Endots LLC
What a tamper-evident expense receipt actually requires (and what most apps skip)
A friend who runs finance at a small Japanese trading company called me last winter to ask a question that turned out to be more complicated than either of us expected. She was preparing a folder of receipts for an audit. The auditor had asked, about one receipt photo on her phone: how do I know this image wasn't edited last week to show a different amount?
She didn't have a good answer. The receipt was a JPG in iOS Photos. The capture date in the EXIF metadata was unverifiable — phone clocks can be set to any value. The image file could have been opened in Photoshop and re-saved at any time without leaving an obvious trace. There was nothing in her workflow that proved the photo she was showing the auditor was the photo she'd taken at the restaurant.
She passed the audit. The auditor was reasonable and the amounts were small. But the question stuck with me because it pointed at something most expense apps don't actually handle: a receipt photo is not, by default, evidence. It's an illustration of evidence. Treating it as evidence requires deliberate design decisions that most apps skip.
The four properties an audit-grade receipt needs
When I sat down to think about what would make a receipt record actually defensible — not just visually plausible but verifiable — I ended up with four requirements:
Integrity. The bytes haven't changed since capture. If the receipt is a JPG of a paper restaurant bill, the JPG file you're showing the auditor is bit-for-bit identical to the JPG that was captured at the moment of payment. No edits, no re-saves, no quiet substitution.
Provenance. When it was captured, by whom, on what device. Not just a self-reported date in metadata, but a record that an outside party could verify.
Reproducibility. Someone other than the user can verify the integrity claim. The user can't be the sole party who attests that the receipt is the original; that's circular. There needs to be a hash, a signature, a checksum — something a third party can recompute against the file and confirm matches.
Independence from the user's clock. Timestamps can't be set by the person who uploaded the receipt. If the receipt's recorded timestamp is from the user's phone, the user (or anyone who controls the user's phone settings) can backdate it. The timestamp has to come from a clock the user does not control.
These four sound obvious when listed. The interesting question is how most expense apps fail each of them.
How most apps fail
Integrity. Most apps store receipts as regular image files in their backend, queryable and downloadable by anyone with file-level access. There is no recorded hash at capture; if the file is replaced later, no one knows. The app's database might log "receipt uploaded at time T," but the database has no way to prove that the bytes it has now are the same bytes it had at time T. This is true even for apps with reasonable security hygiene — the issue is not access control, it's the absence of any after-the-fact verification mechanism.
Provenance. Client-side timestamps are user-controllable. iOS and Android let you change the phone's clock; image EXIF metadata reads that clock when the photo is taken. If a user changes the phone clock backward by a week, takes a photo, and uploads it, the EXIF date will read a week earlier and the app will record it as such. Server-side timestamps would solve this if apps used them, but many don't — they trust the client's claim about when the photo was taken because trusting client-supplied data is easier than coordinating server time and the user experience suffers slightly if you don't.
Reproducibility. This is the cleanest gap. Almost no consumer expense app publishes a hash of each receipt at capture time. Without a hash, there is nothing for a third party to verify against. An auditor cannot say "let me recompute the SHA-256 of this file and confirm it matches what your system recorded at capture" because the system never recorded a hash.
Clock independence. Even apps that do record server-side timestamps often don't make this fact visible in the UI. The user sees a date that could be either the client-claimed timestamp or the server timestamp; without a clear UI distinction, the audit trail is ambiguous.
How Zenrate handles each
When I designed Zenrate's receipt model I started from these four requirements and worked backward to the implementation.
Integrity is handled by computing a SHA-256 hash of the receipt image at the moment of capture and storing the hash alongside the file. The hash is computed client-side as part of the upload flow, immediately after the image bytes are read from the camera or filesystem, and before the bytes hit any storage. The hash is then sent to the server along with the image. Anyone — the user, the auditor, the user's accountant, a future system administrator — can recompute the SHA-256 of the stored file and compare it to the recorded hash. If they match, the file is unchanged. If they don't, it's been modified. The verification doesn't require any trust in Zenrate; it's a property of the hash itself.
Provenance is handled by recording the upload timestamp from the database server's UTC clock, not from the client. When the upload arrives at the server, the database row is created with `NOW()` (or the equivalent) from PostgreSQL's perspective — the timestamp is whatever the database server thinks the current time is. The user has no control over the database server's clock; they can't backdate the record by changing their phone settings. The image's EXIF date is still recorded separately, for context, but the official "this receipt was uploaded at" timestamp is the server's.
Reproducibility falls out of the integrity decision. The hash is stored as a string field on the receipt record, visible in the UI alongside the image. Anyone with the image and the hash can verify the match using a one-line command (`shasum -a 256 receipt.jpg` on macOS or Linux, `Get-FileHash receipt.jpg -Algorithm SHA256` on Windows). No special tooling, no Zenrate API access required. The hash format is the standard hex-encoded SHA-256 output, which any auditor's tooling will already understand.
Clock independence comes from the server-side timestamp decision plus a UI choice to label timestamps as "captured by client" vs. "recorded by server." The receipt record has both. The client-side timestamp is from the user's device and reflects when the photo was taken; the server-side timestamp is from the database and reflects when the upload was received. Both are kept; both are shown. An auditor can see at a glance which is which and weight them accordingly.
The rate metadata is part of the record
The four properties above are about the image itself, but a receipt is more than an image. For an expense receipt with a foreign-currency amount, the record also needs to capture the math that translated that amount to home currency. Zenrate stores, alongside the image and its hash:
- The rate source (e.g., `mizuho`, `ecb`, `us_treasury`)
- The rate policy (e.g., `transaction_date`, `prior_month_end`)
- The rate value used at the time of entry
- The lookup date the policy resolved to (which may differ from the transaction date — for prior-month-end, it's the closing date of the previous month)
These four fields are immutable for the life of the receipt record. If the user later changes the account's default rate source or policy, existing receipts keep the values that were active when they were recorded. We don't retroactively re-translate; that would silently change historical book values, which is exactly the kind of thing tamper-evidence is meant to prevent.
Together, the image (with its hash and server timestamp) and the rate metadata form a complete audit trail for one transaction. The auditor can verify the image is unmodified, see when it was uploaded, see exactly which rate source and policy produced the home-currency amount, and recompute the math if they want to.
What this looks like in the UI
The receipt detail page in Zenrate shows the image, the SHA-256 hash, both timestamps, and the rate provenance, all as visible fields. The hash is the kind of thing a user will never look at in normal workflow — but when an auditor asks, the answer is on the page, not buried in a database log. The hash is also shown when the user exports the receipt or generates a PDF expense report; the PDF carries the hash text alongside the image, so the audit trail survives independently of Zenrate's database.
There's an `IntegrityBadge` component (in `apps/web/src/components/IntegrityBadge.tsx` if you're reading the codebase) that displays a green check next to the receipt if the stored hash matches a recomputed hash of the currently stored image, and a red warning if it doesn't. This makes any inadvertent modification — a corrupted file, a re-encoded image somewhere in the pipeline — immediately visible.
Honest limitations
Zenrate's receipt records are tamper-evident in the cryptographic sense: any modification to the stored image will fail verification against the stored hash. They are not, however, tamper-proof. A malicious actor with sufficient access to Zenrate's database could in principle replace both the image and its hash simultaneously, producing a record that passes self-verification while no longer corresponding to the original. Defending against this would require either:
- Signing each receipt with an external Time Stamping Authority (RFC 3161 timestamping), so the timestamp and hash are vouched for by a party other than Zenrate.
- Chaining receipt records in a Merkle-tree-like structure so that modifying any one record requires modifying all subsequent records, making tampering trivially detectable.
Neither is implemented today. For the SME and freelance use cases that Zenrate is designed for, the current model is defensible: it raises the cost of receipt tampering from "free and untraceable" to "requires database-level access and coordinated modification of multiple fields," which is enough for most small-business audit contexts. For strict government archival requirements or for evidence that needs to survive adversarial scrutiny by a sophisticated attacker, additional layers would be needed. We document this openly rather than implying parity with archival-grade systems.
A small note on the privacy-first migration
Zenrate's storage architecture went through a privacy-first migration in 2024. Receipt image bytes now live in browser IndexedDB by default; only the SHA-256 hash is sent to the server. This means the integrity proof is preserved (anyone with the image can rehash and verify) but Endots LLC, as the operator, cannot see the image contents. For users who want cross-device sync, the image bytes are stored in Supabase Storage with row-level security; the hash-only path remains the default for users who don't enable sync.
This changes the threat model in a specific way: a database compromise on Zenrate's side does not expose receipt images to an attacker, only their hashes. The integrity property survives the migration unchanged.
Why this matters for Zenrate
Tamper-evidence isn't a marketing feature; it's the minimum bar for treating expense records as evidence rather than illustrations. The SHA-256 hash, the server-side timestamp, the immutable rate metadata, and the IntegrityBadge UI together let a user hand their books to an auditor and answer questions about provenance directly from the data. The fact that this requires no trust in Zenrate itself — anyone can rehash and verify independently — is the design property worth preserving as the product evolves.
— Koryu, Endots LLC