# Attachments and the audit trail

> Only NetSuite has a real audit trail through its API. Xero gives you a per-record history feed; QuickBooks gives you a timestamp and no actor at all. Any evidence surface has to be honest about that.

Source: https://lopifi.com/apis/attachments-and-audit
Part of: Lopifi — US GAAP books on autopilot, reviewed by a CPA (https://lopifi.com)

---

**Can you see who changed what, and get the source document?** Never claim complete history. State what we can see, and when we last looked.

## Divergences between Xero, QuickBooks Online and NetSuite

### QuickBooks has no audit trail in the API

There is no endpoint for change history and no actor on any record. If a bookkeeper edits an invoice in QuickBooks, you will see a new LastUpdatedTime and nothing else — not what changed, not who changed it, not what it was before.

**Handling:** Keep a before-and-after for every change we make and snapshot watched fields on every sync, so a diff can be reconstructed. Present it as our record, not the ledger's.

### Attachments are the largest thing you will sync

Files dominate an integration's storage and transfer cost, and QBO needs an extra query per transaction to discover them. Fetching every attachment eagerly on first connection is how a sync exceeds a daily call cap on day one.

**Handling:** Sync attachment metadata eagerly and file bytes lazily, on first view or on an explicit archive action. Show a placeholder that is honest about the file not being fetched yet.

## Canonical shape

```ts
type Attachment = {
  id: string;
  fileName: string;
  mimeType: string;
  bytes?: number;
  /** Metadata syncs eagerly; content is fetched on demand. */
  contentUrl?: string;
  fetchedContent: boolean;
  source: SourceRef;
};

type AuditEvent = {
  at: string;                    // ISO instant
  actor?: { id: string; name: string };   // absent on QBO, always
  action: "created" | "updated" | "voided" | "deleted" | "approved" | "synced";
  field?: string;                // NetSuite systemnote is field-level
  before?: string;
  after?: string;
  /** Ours or theirs. The UI must not blur the two. */
  observedBy: "us" | "source";
};
```
