Two people are looking at what they both call the same invoice, and reading different totals. One has a PDF from March. The other has the record on screen. Somewhere between the two, something moved — and nobody can say what moved, when it moved, or who moved it.
This is not really an invoicing problem. It's the same problem with a contract, a price list, a purchase order, a patient record, a config file, a signed quote: two copies that are supposed to be identical, and no cheap way to know whether they still are.
There's a proper answer, and it's older than any of the software you're running. But it only works if you get one unglamorous detail right, and almost everyone gets that detail wrong.
Three questions hiding inside one
"Did this invoice change?" is actually three questions, and they cost wildly different amounts to answer.
- Did anything change? A yes or no.
- What changed? A list of fields, old value and new.
- Who changed it — and could they hide it? An audit trail you can trust even when the person you're auditing owns the database.
Most systems are built to answer the first, described as though they answer the second, and quietly sold as though they answer the third. Keeping them separate is most of the work.
The answers that don't work
Reading it. A human comparing two invoices catches the total and misses the tax rate. Nobody proofreads the twelfth line item at 6pm.
The updated_at column. This is the reflex, and it's weak. It records that a write happened, not that anything changed — someone opens the invoice, saves it unmodified, and the timestamp moves. Worse in the other direction: a correction applied straight to the database leaves it untouched. It's also useless across parties. You can't email your customer an updated_at and ask whether their copy matches.
Comparing field by field. This actually works, and it's still a bad foundation. You have to keep the old copy in full. You have to compare n fields, and keep that comparison in sync forever — add a discount column next quarter, forget to add it to the comparator, and changes to it become invisible. Silently. That's the failure mode that matters: not a check that breaks loudly, a check that stops looking and says everything's fine.
And you can't hand it to anyone. There's nothing compact to exchange. To ask "does your copy match mine?" you have to send your whole copy.
A fingerprint for a document
What you want is a small thing that stands in for a big thing.
A hash function takes an input of any size — three words, a whole invoice, a film — and produces a fixed-length string. SHA-256, the common choice, always produces 64 characters. Three properties make it useful here:
- The same input always produces the same output.
- Changing anything in the input — one digit, one space — produces a completely different output. Not a similar one. An unrecognisable one.
- You can't work backwards from the output to the input.
You've seen this without noticing. That long string of letters and numbers next to a software download, the one nobody clicks — that's a fingerprint of the file, so you can check the thing you downloaded is the thing they published.
Here's an invoice reduced to its essentials. Amounts are in paise, whole numbers, no decimals — I'll come back to why.
{"currency":"INR","items":[{"amount":4500000,"desc":"Consulting","qty":1}],"number":"INV-2026-0184","total":5310000}
72feee8869d5dd0e7c4507a8e8bc48a0246d97ca18421085dcb7d48c697bebb7
Now raise the line amount by exactly one rupee, and the total with it:
{"currency":"INR","items":[{"amount":4500100,"desc":"Consulting","qty":1}],"number":"INV-2026-0184","total":5310118}
1269088c28e87d680d907ba60299ac9b2dd2164f8ce2e700aac4857feb1436d1
Not one character in common. A one-rupee change and a hundred-thousand-rupee change are equally loud, which is the entire point — the fingerprint has no opinion about which fields you consider important. It notices everything with the same intensity.
These are real values, not illustrations. Paste this into a terminal and you'll get the first fingerprint back:
printf '%s' '{"currency":"INR","items":[{"amount":4500000,"desc":"Consulting","qty":1}],"number":"INV-2026-0184","total":5310000}' | shasum -a 256
Store that 64-character string when you issue the invoice. Recompute it whenever you want to know. Match means nothing changed. No match means something did. One comparison, no old copy retained, and short enough to print in a footer or read down a phone.
Same invoice, different bytes
Here is where implementations fail, and it's worth slowing down for.
The hash doesn't see your invoice. It sees bytes. And the same invoice can be written as bytes in a great many ways.
That third block above — same invoice as the first, same fields, same values, just written with the keys in a different order:
{"number":"INV-2026-0184","currency":"INR","total":5310000,"items":[{"desc":"Consulting","qty":1,"amount":4500000}]}
d1401d528ddc581230360f42ab963a48cfcb5e3682d24b44cd4281bad98ff768
Nothing changed, and the fingerprint says everything changed.
That's an alarm nobody can act on, and after the third false alarm people stop looking — which leaves you with a check that costs money and detects nothing. The opposite failure exists too, and it's worse: two systems that each hash their own way will never agree, so you conclude comparison is impossible and switch it off.
So before hashing, the document has to be written in exactly one agreed form. Every time, on every machine, in every language. That's canonicalisation, and it's the actual engineering:
- Field order is fixed. Sort keys. Two services building the same invoice from the same data must emit the same bytes.
- Money is an integer. In the smallest unit — paise, cents. Never a decimal, and never a floating-point number:
0.1 + 0.2genuinely does not equal0.3in most languages, and45000.00,45000.0and45000are three different byte strings for one amount. Integers have none of these problems. - Dates are one format. ISO 8601, UTC.
15/08/2026,2026-08-15andAug 15, 2026are the same day and three different fingerprints. - Text is normalised. An accented character can be stored as one code point or as two, looking identical on screen and differing in bytes. Pick one form (NFC) and convert everything to it.
- Absent is not empty. A missing field,
null, and""must not be three different things. Decide, and apply it everywhere.
If you'd rather not invent these rules, RFC 8785 — the JSON Canonicalization Scheme — has already written them down, and there are implementations in most languages. Use it before you write your own.
What counts as a change?
The fingerprint forces a question the business has usually never been asked out loud: which fields are the invoice?
The amount, obviously. The tax rate, the line items, the buyer, the date. But what about the internal note someone added? The number of times it's been viewed? The updated_at column itself — include that and the fingerprint changes every time anyone breathes on the record, which makes it worthless.
Include too much and it screams constantly. Include too little and a real edit slips past in silence. There's no technical answer to this. It's a business decision about what the document is, as opposed to what your database happens to store alongside it — and the value of doing this work is partly that somebody finally has to decide.
Write that field list down. Version it. When it changes, store which version of the field list produced each fingerprint, or you'll be comparing fingerprints from two different definitions and drawing confident nonsense from the result.
Which line moved?
One fingerprint answers question one and stops. To get question two — what changed — without keeping full copies, fingerprint the parts.
Hash each line item on its own. Then hash the list of those hashes to get the invoice's overall fingerprint. Now a mismatch at the top lets you walk down: compare the item fingerprints, find the one that doesn't match, and you've isolated the change to a single line without ever storing the old invoice.
That structure — hashes of hashes — is a Merkle tree. It's what lets a system with millions of records find the handful that differ without comparing all of them, and it costs you very little to adopt on day one.
A history that checks itself
Now make each revision record the fingerprint of the one before it.
Revision 4 contains revision 3's fingerprint. Revision 3 contains revision 2's. To quietly rewrite revision 3, you'd have to change its fingerprint — which is written into revision 4, whose fingerprint is written into revision 5, and so on. One edit invalidates everything after it. The history stops being a list you're asked to trust and becomes a structure that proves itself.
This is exactly what Git does. Every commit's id is derived from its content and its parent's id, which is why you cannot alter old history without every subsequent commit changing too.
What a fingerprint can't do
This is the part usually left out, so let me be plain about it.
A hash detects change. It prevents nothing. If someone can edit the invoice and also edit the stored fingerprint, they recompute both and there is nothing to find. Any system where the same database holds the record and the record's fingerprint offers no protection against whoever administers that database. It catches accidents, bad migrations, sync bugs, and outsiders. Not insiders.
For that, the fingerprint has to live where the person editing can't reach:
- Signed, with a key held elsewhere — a keyed hash (HMAC) or a digital signature. Now producing a valid fingerprint requires the key, not just the algorithm.
- In an append-only log — write-once storage, or a separate system with separate credentials, where entries can be added and not rewritten.
- In someone else's hands — the simplest of the three. The fingerprint on the PDF you emailed the customer in March is beyond your reach now. That's what makes it evidence.
And a caveat on the algorithm: use SHA-256 or better. MD5 and SHA-1 are broken in the specific sense that matters here — it is practically possible to construct two different documents with the same fingerprint, which is precisely the guarantee you were relying on.
Where it actually goes
Once you have one, it earns its keep in more places than you built it for.
Print it in the invoice footer, and any copy can be checked against the record. Return it as an HTTP ETag, and clients stop re-downloading unchanged documents. Use it as an idempotency key, and you stop emailing the customer an invoice that didn't actually change. Compare it across systems, and reconciliation becomes one string comparison instead of a spreadsheet and an afternoon.
The pattern generalises to anything that has to stay the same and can't be trusted to. Sixty-four characters, computed once when you issue the document, that answer a question people otherwise settle by arguing.
The work isn't the hashing. Every language has that in one line. The work is deciding exactly what the document is, writing it down the same way every time, and being honest about which of the three questions you've actually answered.
The name on the door
There's a reason this post lives on this particular site.
Applied to software rather than paperwork, the fingerprint has a name: a version hash. Every commit in a codebase carries one, derived from its contents and its parent's — the same construction as the chained invoice revisions above. It's how you answer which version is actually running in production with a fact instead of a belief. Not "the latest." Not "v2.1." Those are labels, and a label is only as good as the person who last moved it. A version hash points at one exact state of one exact thing, and stops matching the instant anything about it changes.
That's where our name comes from. Version# — the identifier of precisely one version, that changes the moment anything does.
It's also a fair standard to hold us to. A value you can check yourself beats an assurance you have to take on trust.
Frequently asked
- How can I tell if an invoice has been changed?
- Reduce the invoice to a fixed set of fields, write those fields in one agreed format, and run them through a hash function. The result is a short string — a fingerprint. Store it. If you recompute it later and it doesn't match, something in those fields changed. If it matches, nothing did.
- Isn't an updated_at timestamp enough to detect changes?
- No. A timestamp records that a write happened, not that anything meaningful changed — re-saving identical values bumps it, and a direct edit to the database can leave it untouched. It also tells you nothing about what changed, and can't be compared against another party's copy.
- Does hashing an invoice make it tamper-proof?
- Not on its own. A hash detects change; it prevents nothing. Anyone who can edit the invoice and also edit the stored hash can recompute both and leave no trace. Tamper-evidence needs the fingerprint held somewhere the editor cannot reach — signed with a key they don't have, written to an append-only log, or already in the other party's hands.
- What does the name Version# mean?
- A version hash is the fingerprint of one exact version of something. In software, every commit in a codebase carries one, derived from its contents and its parent's — which is how you know precisely which version is running, rather than trusting a label like 'v2.1' that anyone can move. Version# is named after that idea: an identifier that points at one exact state of one exact thing, and changes the moment anything about it changes.