The Invoice Audit Trail: Every Event Logged Behind the Scenes
In this article
Every time an invoice is viewed, commented on, or has its status changed, that event gets written to a permanent audit log tied to the invoice — independent of, and in addition to, the invoice's own edit history. This log isn't surfaced as a single polished "activity feed" screen in every part of the product, which means most users never see it directly, but it's quietly recording who did what, when, and from where, for every document you send out. Here's what actually gets captured, and what it's useful for.
Two Separate Tables, Two Separate Purposes
The audit trail is really two related but distinct logs. The first records discrete events — an invoice being viewed, a comment being added, an estimate being approved or rejected — each as a single row with an event type, an actor, and a timestamp. The second records document views specifically, and does so with more detail than the general event log: it captures the IP address and user-agent string of whoever loaded the document, along with whether they got there through a share link or through the client portal. The two overlap by design — a client-portal view generates a row in both tables — but the view-specific table exists because view events happen far more often than any other kind, and carry information (IP, user agent, access method) that doesn't apply to a comment or a status change.
Actor Types: User, Client, or System
Every logged event is tagged with one of three actor types. "User" events are things you or a teammate did while logged into the dashboard — editing an invoice, marking it paid manually, changing its status. "Client" events are things that happened through a share link or the client portal, without anyone needing an account — a client opening a shared invoice, leaving a comment, or approving an estimate. "System" events are things the application did on its own, without any human directly triggering them in the moment — an automated reminder being sent, a recurring invoice being generated, a late fee being applied. This three-way split matters when you're trying to reconstruct what happened to an invoice: a status of "viewed" tagged as a client action tells you your client actually opened the document, whereas the same event tagged as a system action might just mean an automated process touched the record without a human looking at anything.
What Gets Recorded on a View
A view event captures which invoice was viewed, whether the view came through a share link or the client portal, the specific token used if it came through a share link, the viewer's name and email if those were available (a client portal session usually has them; an anonymous share-link open often won't), the requesting IP address, and the browser's user-agent string. The IP address is extracted with a preference for the X-Forwarded-For header when the app is running behind a proxy or load balancer, falling back to the raw connection address otherwise — this is standard practice for correctly identifying the real client IP in a hosted environment rather than logging the address of an internal proxy.
None of this is exposed as a live "who's viewing this invoice right now" indicator anywhere in the current interface — it's captured for the record, not surfaced as a real-time notification, though the invoice share link feature separately maintains a simple view count and last-viewed timestamp specifically so you have a lightweight read receipt without digging into the full audit log.
Failure Handling: The Log Is Best-Effort, Not Blocking
Writing an audit event is wrapped so that if the write itself fails for any reason — a database hiccup, an unexpected data shape — the error is caught and logged to the server's own error output, but the underlying action (the view happening, the comment being saved) is never blocked or rolled back because of it. This is a deliberate trade-off: the audit trail is a record of what happened, not a gatekeeper for whether it's allowed to happen. A client should never be unable to view an invoice, or leave a comment, because of a transient problem writing to a logging table. The cost of that trade-off is that in a rare failure scenario, an event could occur without a matching log entry — but the alternative, where a logging failure could block a client from seeing their own invoice, is considered the worse outcome.
Why There's No Single "Activity Feed" Screen
Because the audit events and view events live in normal database tables tied to each invoice, the data supports building an activity timeline for any given document — and pieces of it already surface indirectly, like comment threads and share-link view counts. What doesn't currently exist is one consolidated screen that renders the full raw event stream (every view, every comment, every status change, in strict chronological order with actor and IP shown) as a single readable list. If you need that level of detail today — for instance, to establish exactly when a client opened a disputed invoice — the data exists in the underlying tables and can be pulled directly, even though there's no polished UI in front of it yet for every use case.
What This Is Useful For
The most practical use of this log is dispute resolution: if a client claims they never received or never saw an invoice, the view log is a factual record of whether a view actually happened, when, and from what IP and browser — evidence that's considerably harder to argue with than "I sent it, so you must have gotten it." It's also useful for understanding client behavior patterns across a relationship — a client who reliably opens an invoice within hours of it being sent behaves very differently, from a collections standpoint, than one whose invoices sit unopened for weeks, and that's a pattern the underlying data can reveal even without a dedicated analytics view built on top of it yet.
What It Isn't
This isn't a security audit log in the sense of tracking login attempts, password changes, or permission changes at the account level — that's a different concern handled elsewhere. It's specifically scoped to individual invoices and the actions taken on or around them, which is a narrower and more directly useful scope for the most common question people actually have: what happened to this particular document, and who saw it.
How This Relates to Share-Link View Counts
Invoice share links maintain their own lightweight view count and last-viewed timestamp, separate from the full audit trail described here. That simpler counter exists specifically so you can glance at a share link's details and immediately see "has this been opened, and when" without needing to query the underlying event log at all. The full audit trail is the more detailed, queryable record behind that summary — every individual view that contributed to that counter is also logged as its own row in the document-view table, complete with IP address, user agent, and access method. Think of the share-link view count as the headline number, and the audit trail as the detailed receipts behind it: for a quick check, the counter is enough; for a genuine dispute or a detailed reconstruction of exactly when and how a document was accessed, the underlying log is where the real evidence lives.
Actor Names and Emails Aren't Always Available
Because client-side views and comments often come from someone who was never required to create an account — a share link can be opened by anyone with the URL, with no login involved — the actor name and email fields on a given event are frequently empty for pure share-link traffic. Client portal activity tends to carry more identifying detail, since a client portal session is generally tied to a specific known client record rather than an anonymous link visit. This is expected and not a gap in the logging: the system records whatever identifying information was actually available at that moment, rather than requiring identification it doesn't have. A comment left through the client portal will usually show a name and email; an anonymous share-link view might only show an IP address and a browser string, and that's still useful information on its own even without a name attached to it.
Metadata Is Stored as Flexible JSON, Not Fixed Columns
Beyond the core fields every event shares — actor, type, timestamp, invoice — each event also carries a metadata field, stored as a JSON blob rather than a fixed set of database columns. This is what lets very different event types share the same underlying table without needing a new column added every time a new kind of event is introduced: a view event's metadata might hold the source type and token, while a status-change event's metadata might hold the old and new status values. If metadata ever fails to serialize cleanly for some reason, the write falls back to wrapping whatever value it has in a simple object rather than letting a serialization edge case break the whole log entry — another example of the audit trail being built to degrade gracefully rather than fail loudly.
Related Articles
How the Comment Notification Digest Batches Client Activity Into One Email
Why a burst of client comments produces exactly one email, not five — and how the rolling delay resets on every new comment.
How In-App Notifications Fan Out to Your Team
Why every workspace member gets their own independent notification row, and why you don't get notified about your own actions.
How Two-Factor Authentication Protects Your Account
2FA generates a six-digit code that changes every 30 seconds using the TOTP standard — no live connection between your phone and the server is ever required.
How API Keys Are Stored (And What to Do If You Lose One)
The raw value of your API key is never stored anywhere after the moment you create it — only a one-way hash is kept, which is why a lost key can't be recovered.
Is It Safe to Put a QR Code on Your Invoice?
If you've thought about adding a QR code to your invoices, you've probably also seen a headline or two about "quishing" — QR code phishing — and wondered whether you'd be handing your clients a security risk along with your bill. That's...
Building Reliable Integrations With Invoice Webhooks
Webhooks look simple from the outside: something happens, you get a POST request, you do something in response. The complexity shows up once you start asking what happens when that POST request doesn't arrive, arrives twice, or arrives w...