Building an Invoice Audit Trail Your Tax Authority Will Trust

If you've ever pulled an invoice up six months later to "fix a small thing" and re-saved it, you've already done something most tax authorities don't allow. The rule across the EU, the UK, and most of the OECD is the same: an issued invoice is a primary tax document, and once it's been delivered to the recipient, it should not change. Corrections happen by issuing a credit note and a fresh invoice — not by editing the original.

Most invoicing tools don't enforce this. They let you edit any invoice at any time, with no record of what changed. Which means in an audit, you have no way to prove the invoice the auditor is reading is the same one the customer paid against.

This post walks through what a usable audit trail actually looks like, why a hash chain is a particularly good fit for invoicing, and what an accountant can do with one.

What "tamper-evident" means

Tamper-proof is impossible — anything stored on a disk you control can be rewritten by someone with disk access. Tamper-evident is the realistic goal: if someone rewrites a historical record, the manipulation is detectable.

The standard pattern for this is a hash chain. When you issue an invoice, you take a snapshot of the immutable fields (number, parties, dates, line items, total) and compute a SHA-256 hash. You combine that with the hash of the previous entry in the chain. The result is each entry's "entry hash," and every entry depends on every entry before it.

If anyone rewrites an old entry — to change a date, alter a total, hide an invoice — every subsequent entry's hash no longer matches. Walking the chain end-to-end and recomputing the hashes catches it immediately. There's no need to trust the audit log: it self-verifies.

What goes in the snapshot, and what doesn't

This is the part that most people get wrong. The snapshot should contain the fields that should not change after issuance, and nothing else.

Include: invoice number, issue date, due date, supply date, currency, the from/to party details (name, address, tax ID), line items, subtotal, tax rate, tax amount, total, notes, any FX snapshot used for translation.

Exclude: status (draft/sent/paid/void), payment date, reminder history, last-viewed timestamp, internal tags, anything else that legitimately changes during the invoice's life cycle.

If you include lifecycle fields, every status change registers as a "mutation" and the chain becomes noisy. If you exclude immutable fields, you've defeated the point. The boundary needs to be drawn deliberately.

Detecting two kinds of drift

A hash chain catches changes to entries already in the chain. But it doesn't catch two other things on its own:

Post-issue mutations on the live invoice row. If your live database still allows edits to an issued invoice, the chain entry stays correct but the live row drifts. To catch this, periodically compare the live row's immutable fields against the snapshot and flag mismatches. This isn't a chain operation; it's a separate diff.

Sequence gaps. Tax authorities expect invoice numbers to be contiguous within a prefix-year. INV-2026-001, -002, -003, …, with no gaps. If invoice -003 was deleted or never issued, the audit needs to surface that — and ideally explain it.

Sequence-gap detection is straightforward: enumerate the issued numbers, find missing integers in each prefix-year, list them. A periodic report makes this visible at a glance.

What changes get classified, and how to prioritise

Not every change is equally bad. A typo fix in the notes field is annoying but not financially material. A change to the total or the recipient's name is a much bigger problem. A workable audit log distinguishes between them:

  • Financial — total, tax amount, line items, currency. Loud alert.
  • Recipient identity — recipient name, address, tax ID. Loud alert.
  • Issuer identity — your own name, address, tax ID. Less common but still loud (someone has been editing your business profile).
  • Metadata — notes, internal tags. Quiet flag.

In the UI, the financial category should be coloured differently — red rather than amber — and rise to the top of the list. An accountant looking at the audit log in five minutes should see the worst case first.

The downloadable audit pack

This is the part your accountant actually wants. A single file they can verify independently, without a login to your platform, using tools they already have on their laptop.

What goes in it:

  • The full chain of issuance entries — entry hashes, payload hashes, prev hashes, timestamps.
  • Per-entry snapshots of the immutable fields.
  • The sequence-gap report.
  • The post-issue mutation list.
  • A verification recipe: which algorithm to use (SHA-256), what input to feed it (canonical JSON of the snapshot, in key-sorted form), how the entry hash is constructed.

JSON is the right format. It has no dependencies, every developer's toolchain reads it, jq and python -m json.tool are universally available, and the verification recipe is something an accountant's CTO can implement in twenty lines of any language.

ZIP isn't worth the dependency. PDF defeats the machine-verifiable property. JSON is the floor.

What an accountant does with it

Three things, in increasing order of how much they care:

Spot-check. Pick a random invoice, recompute its payload hash from the snapshot, confirm it matches the recorded payload hash, and walk one or two prev-hash links to confirm the chain is intact. Five minutes.

Full verification. Run a 20-line script that walks every entry, recomputes both the payload and entry hashes, and reports any mismatch. Adds confidence before signing off accounts.

Reconciliation against bank records. Use the immutable totals to confirm that what was invoiced matches what was received. This is the work they were going to do anyway; the audit pack makes it auditable.

Why not just sign every entry with a private key?

Cryptographic signing — HMAC with a server-side secret, or asymmetric signing with a private key — sounds stronger than hashing. In practice for an invoicing tool, it doesn't add much.

A hash chain protects against post-hoc edits by anyone who can write to the database. A signing key protects against forgery by anyone who has database write access but not the signing key. The threat model is: a database administrator has decided to silently rewrite history. If that's your threat model, you have bigger problems than an audit trail.

Signing helps when the verifier doesn't trust the issuer. For an invoicing tool whose verifier is the issuer's own accountant — already on the issuer's side — signing is largely ceremonial. Hashing is sufficient.

When this matters most

Day to day, you'll probably never look at the audit ledger. The case where it earns its keep is the audit itself: a tax authority showing up wanting to know why invoice INV-2025-127 has a total of €1,847.50 instead of €1,475.80 like the bank record suggests.

The answer should be a single click. "Here's the chain. Here's the snapshot from issuance. Here's the live row. Here's exactly what changed, when, and by whom." Without an audit trail, that conversation is far worse.

What Plain Statement does

Every invoice that transitions to "sent" is snapshotted into an append-only ledger and hash-chained to the previous entry. The /api/integrity endpoint walks the chain, detects post-issue mutations, and reports sequence gaps. The downloadable audit pack is a single JSON file your accountant can verify with openssl and jq — no Plain Statement account required on their side. Existing invoices are backfilled into the chain in sent-date order, so the audit surface is meaningful from day one.

The audit ledger and downloadable audit pack are part of the Basic plan.

Create a professional invoice in under 2 minutes — no account required.

Create Invoice Free