Overview
The audit log is the workspace’s memory. Every meaningful CREATE, UPDATE, or DELETE on a first-class entity — an order, a client, a vendor, a payment, an integration key, a team role — is recorded as an immutable row, hash-chained to the row before it. You can read it, filter it, export it, hand it to an auditor, and verify nothing has been tampered with.
It’s the surface that powers compliance reviews, dispute investigations, and internal “wait, who changed that?” questions. It’s also how we make decisions reversible: every entry includes a before/after diff so you can see exactly what changed.
What working correctly looks like
- Creating, updating, or deleting an order writes a row in Settings → Audit log within a second.
- The integrity check at Settings → Audit log → Verify chain returns Chain intact — every row’s previous-hash matches the prior row’s current-hash.
- Filtering by
actor,entity_type,action, or date range narrows the list immediately. - Daily archives appear under Settings → Audit log → Archives, one file per day.
- Sensitive fields (emails, phone numbers, addresses) are masked unless you have admin permission to see them in full.
- Sign-in and MFA events from
auth-audit-hookshow up alongside operational events — same chain, same view.
What gets logged
Anything that changes a first-class entity. The list below is illustrative, not exhaustive — new event types are added as the product grows.
Operational events
- Orders —
order.created,order.updated,order.cancelled,order.refunded,order.shipped,order.delivered. - Clients & vendors —
client.created,client.merged,vendor.created,vendor.archived, etc. - Payments —
payment.created,payment.refunded,payment.disputed. - Communications —
email.sent,email.bounced,email.complaint_received,sms.sent,sms.optout_received. - Settings —
settings.email.changed,settings.sms.changed,settings.translation.cap_changed, etc.
Security events
- Authentication (via
auth-audit-hook) —auth.sign_in,auth.sign_out,auth.mfa_challenge,auth.mfa_success,auth.mfa_failed,auth.password_reset,auth.recovery_code_used. - Team —
team.invited,team.invite_accepted,team.role_changed,team.removed,team.store_access_changed. - Keys & webhooks —
integration_key.created,integration_key.rotated,integration_key.revoked,webhook.endpoint_changed.
Billing events
billing.plan_changed,billing.payment_succeeded,billing.payment_failed,billing.subscription_cancelled,billing.module_flag_changed,billing.quota_warning_50/_80/_100.
Each row carries
id— monotonically-increasing chain index.at— UTC timestamp.org_id+store_id.actor—user:<uuid>,integration_key:<uuid>,system(for cron/webhook actors), orauth_hook.entity_type+entity_id.action— the verb (created,updated…).before+after— JSON diff of the affected fields.request— IP, user-agent, request id.prev_hash+hash— the chain links.
The hash chain
Each row’s hash is computed as sha256(prev_hash || canonical(this_row_minus_hash)). Tamper with any row in the middle and every subsequent prev_hash stops matching, which is exactly what an auditor wants.
row 0: prev_hash = 0x000… hash = H0 row 1: prev_hash = H0 hash = H1 row 2: prev_hash = H1 hash = H2 … row N: prev_hash = H(N-1) hash = HN To verify: walk forward, recompute each hash, ensure prev_hash on row k equals hash on row k-1.
Run the verifier at Settings → Audit log → Verify chain. Two outputs:
- Chain intact — the chain through the latest row matches end-to-end.
- Chain break at row # — we point you at the first row where the link fails. This is rare; if you see it, capture the row id and email {{CONTACT_EMAIL}} immediately.
The chain doesn’t prevent a database admin with raw UPDATE permission from rewriting rows. It guarantees that if they do, the chain breaks and we (or you) can detect it. Combined with the daily cold-storage archive, you have a comparison point that makes silent tampering impossible.
Viewing the log
Settings → Audit log is a virtualized list with the following filters in the top bar.
- Actor — user, integration key, or system. Type-ahead.
- Entity type — pick from a dropdown of all known types in your workspace.
- Action — the verb.
- Date range — presets (24h, 7d, 30d, 90d) or custom.
- Free-text — matches against entity ids, key handles, IPs.
- Store — defaults to the active store. All stores for cross-store views.
Click any row to open the detail panel: full before/after diff, the request fingerprint, the chain neighbors, and links to the affected entity (which opens with the timestamp pre-filtered).
Export
Two formats from Settings → Audit log → Export:
- CSV — flat rows, one per event. Useful for spreadsheets and accountants.
- JSON — the full structured form including
before/afterdiffs and chain hashes. The right format for compliance reviews.
Exports respect the active filters — if you’re looking at the last 30 days for one user, that’s what gets exported. Files larger than ~50 MB are streamed; you receive an email with a signed download link when the file is ready.
Retention & archives
- Hot retention — the live Settings → Audit log view holds 365 days of rows for fast filtering.
- Cold retention — everything is archived to encrypted cold storage by
audit-archive-cron, which runs daily at02:00 UTC. One file per day, namedaudit/{org}/{YYYY-MM-DD}.jsonl.gz. - Forever — archived files are retained for the lifetime of the workspace plus 7 years on Scale tier (configurable per-contract on Custom). Smaller tiers retain hot for 365 days and cold for 24 months.
You can pull any archive file from Settings → Audit log → Archives; the file is signed and chain-linked at its boundaries, so verifying a single archive day verifies the chain for that day.
Redaction
Some fields that get logged are sensitive. The audit log shows them masked by default and only unmasks for users with the right permission.
- Always-masked even for admins — full credit-card numbers, full bank account numbers, MFA secrets, recovery codes, raw API key plaintext. These never enter the log; only their last-4 / handle / hash do.
- Masked for staff / read_only — client emails, phone numbers, full mailing addresses. Shown to admins in full.
- Always shown — entity ids, action verbs, timestamps, IP cities (not full IPs).
The “Show full PII” toggle is admin-only and the act of toggling is itself audited (audit.pii_unmasked). Every unmask event is recoverable in the log.
The redaction layer covers known sensitive columns. If you paste an API token into a free-form note or message, it’s logged as plaintext — we don’t scan free text. Use the integration-keys flow for tokens.
Meta-audit
Reading the audit log, exporting it, unmasking PII, and verifying the chain are themselves audited. So is anything that adjusts retention or archive behavior. The relevant rows:
audit.viewed— a user opened the log (rate-limited to one per session per hour to keep the chain readable).audit.exported— CSV or JSON export, with the row count and filter snapshot.audit.pii_unmasked— admin toggled “Show full PII”.audit.chain_verified— chain verification was run, with the result.audit.archive_written— the daily cron wrote the day’s archive successfully.
The chain self-protects against deletion: archives include their own start- and end-hash so the daily file is verifiable in isolation.
Settings & permissions
- Settings → Audit log — visible to owner / admin in full;
staffcan see events for entities they have access to (their own actions and the entities they’ve worked on);read_onlysees the same scope asstaffbut with PII masked. - Settings → Audit log → Export — owner / admin only.
- Settings → Audit log → Verify chain — owner / admin only.
- Settings → Audit log → Archives — owner / admin only.
API + automation
Read-only programmatic access via the public API.
GET /v1/audit-events?since=2026-05-01T00:00:00Z&entity_type=order&limit=200 Authorization: Bearer pck_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
The endpoint is keyset-paginated by (at, id), returns the same diff/hash structure shown in the UI, and respects redaction (the key’s scopes decide whether PII is unmasked). For the full request shape and rate limits, see Public API.
There is no “append to audit log” endpoint by design — the chain is platform-managed.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| An action you took isn’t in the log | Action didn’t commit (validation failed silently) | Repeat the action, watch for a toast; if it commits, the row appears within a second. |
| Filter returns no rows for a clearly-real event | Date range too narrow / wrong store filter | Switch to All stores; widen the date range. |
| “Chain break at row #” | Database tampered or row physically corrupted | Capture the row id and email {{CONTACT_EMAIL}} immediately. |
| Daily archive missing for a date | Cron didn’t fire (rare; infrastructure incident) | Email {{CONTACT_EMAIL}} with the date; we re-run the archive from the hot table. |
| Export job email never arrives | Email not delivered (see Email pipeline) or filter matched 0 rows | Run the export with broader filters; check Settings → Email log. |
| PII still masked after toggling unmask | Toggle is admin-only; you’re a non-admin | Ask an admin to share the row, or have them export. |
Related
- Team & roles — team events all show up here.
- Auth & MFA — sign-in and MFA events flow through
auth-audit-hook. - Billing & quotas — plan changes and module flips are audited.
- Multi-store workspaces — every event is stamped with its store id.
- Public API — programmatic read access to
/v1/audit-events.