Audit log
Every change in your organization, who made it and on whose behalf, kept in append-only hash chains you can verify, and visible for your plan's retention window.
Ownrecord records every change made in your organization in its audit log: what happened, to which object, who did it, through which client, on whose behalf, and who approved it. Entries are never changed or removed, and each one is chained to the one before it with a hash, so tampering shows.
Owners and admins read it in Organization → Audit log, and anyone (or any key, agent or app) holding the audit:read permission reads it with audit_log.list (GET /api/v1/audit_log, MCP tool audit_log_list). Object pages in the console (agents, installed apps, approval requests, filing periods, tasks, refunds) link to the audit log filtered on that object.
What is recorded
- every change made through an operation, from the console, the API, MCP, the assistant or a background job;
- API key creation and revocation, secret reveals and rotations;
- approval requests and every decision on them;
- registration state changes, filing period locks and submissions, manual ledger adjustments, refunds and credit grants;
- customer portal links.
Reads aren't recorded, except those Ownrecord staff make under a support-access grant: every request under a grant is recorded with the grant's ID. Requests that were refused (no permission, validation errors) aren't either: they changed nothing.
Each entry has:
| Field | What it is |
|---|---|
action | What happened, usually the operation ID, such as filing_periods.lock. An approval request is approvals.request, with the operation it asks to run. |
target | The object it is about, such as fp_…, re_… or apr_…. |
entityId, brandId | Where it happened. Organization-level changes (members, settings, agents) have neither. |
livemode | The mode of the request that made the change. The log covers both modes. |
before, after | Snapshots of configuration objects before and after the change. Secrets are always redacted. |
ip, userAgent | The request's IP address, truncated to its network (/24, /48), and its user agent. |
requestId | The request that made the change (the Ownrecord-Request-Id it answered with). A change that ran after an approval carries the approving request's ID. |
chain | The entry's place in your organization's hash chains: the chain (0–15), its position and its hash. |
Entries recorded before request IDs were (and changes made by background jobs) have no requestId.
Who did it: the actor
The actor is the whole chain of attribution, the same shape the ledger and events use:
| Field | What it is |
|---|---|
type, id | A person (usr_…), an API key (key_…), an agent (agt_…), an installed app (ains_…) or a job (system). |
via | The surface: console, api, mcp, assistant, portal or job. |
client | The OAuth client the call came through, for agents, apps and MCP clients acting for a person. |
app | The installed app: the actor itself, or the partner app that owns the agent. |
onBehalfOf | The person the call is attributed to: the member who installed the app (or the agent's app). |
approval | For a change that ran after an approval: the request, its status and the people who approved it, in order. |
supportGrantId | The support-access grant a member of Ownrecord's staff acted under. |
summary | The chain in one line, as the console shows it. |
So when an agent that a partner app owns submits a return and a member approves it, the entry shows all four: the agent, the app, the person who installed the app and the approver.
Names (members' emails, agents', apps' and API keys' names, approvals and approvers) are filled in only where the reader may read them elsewhere: members with members:read, agents and apps with agents:read, keys with keys:read, approvals with approvals:read. Everyone who reads the log sees the IDs. Emails are shown for current members only.
Filters
audit_log.list lists entries newest first and takes, in any combination:
actorTypeandactorId: who acted;action: one action, or an area with a wildcard (filing_periods.*);target: one object, the audit log filtered on that object;mode:liveortest;fromandto: whole days in UTC (2026-11-02) or exact times (ISO 8601).
To see one legal entity's or brand's entries only, call it inside that entity or brand (Ownrecord-Entity or Ownrecord-Brand headers, entity_id or brand_id in MCP). A key, agent or app limited to some entities or brands can read their entries this way, and nothing at the organization level. Pages hold up to 100 entries; follow next_cursor. With filters on the actor type, the action or the mode, one call looks at most at 5,000 entries, so a page can come back shorter than limit while has_more is true.
CSV export
Export CSV on the audit log downloads every entry the page's filters match as one CSV file, newest first. Through the API and MCP, audit_log.export (GET /api/v1/audit_log/export, MCP tool audit_log_export) takes the same filters and returns up to 1,000 entries per call as a CSV file in csv; follow next_cursor and append each page's rows without its header. The columns follow the entry and its actor (id, at, action, target, mode, actor_type, actor_id, actor_summary, approval_id, request_id, before and after as JSON, chain_shard, chain_seq, hash and more); names are filled in only where you may read them, as in the list.
The hash chains
Each entry's hash is sha256(previous hash + the entry), so changing, deleting, reordering or forging an entry breaks its chain from that point on. Your organization has 16 chains; an entry goes to the chain of its target, so everything about one object is in one chain, in order. Personal data (the snapshots, IP address and user agent) is kept beside the chain with a salted digest inside it, so it can be erased for a data-protection request without breaking the chain.
audit_log.verify checks the chains now: the newest 100 entries of each chain (every entry when a chain is shorter, and then complete is true), their links, hashes and digests, and each chain's head. The console shows the result as a banner above the log. Every night Ownrecord also verifies every entry of every chain, including entries older than your retention window, and its staff are alerted to any break (AUDIT_CHAIN_BROKEN). The latest nightly result is part of the answer (nightly: when it ran, how many entries verified, where it found a break), and the banner reports a break it found in older entries too; status is broken when either check found one.
Retention
Your plan's audit.retention_days sets how far back the log is visible: 30 days on Sandbox and Starter, 90 on Growth, 365 on Scale, ten years on Enterprise. Older entries are kept, still chained and still verified, but not listed; upgrading shows them again. The response's retention field says the window that applied.