axonpush
Concepts

Audit log

The append-only record of organisation-level change, what is captured, what is deliberately not, the entry shape, and how to page through it without misreading the totals.

Key rotations, invites, role changes, ownership transfers and sign-in attempts are written to an append-only log scoped to the organisation. Admins can read it from the dashboard or from GET /audit-logs.

The log answers “who changed this, and when”. It is deliberately not an event stream: writes are best-effort and a failure to record one never fails the action that triggered it, so the log is evidence rather than a control.

What is recorded

AreaActions
API keysapi_key.created, api_key.revoked
MCPmcp.provisioned, mcp.trace_replayed
Membersmember.invited, member.removed, member.role_changed, member.invitation_cancelled, member.ownership_transferred
Organisationorganization.created, organization.updated, organization.deleted
Appsapp.created, app.updated, app.deleted
Channelschannel.created, channel.updated, channel.deleted
Webhookswebhook_endpoint.created, webhook_endpoint.deleted
Export destinationsexport_destination.created, export_destination.updated, export_destination.deleted
Authauth.signup, auth.signin, auth.signin_failed, auth.google, auth.google_failed, auth.sso, auth.sso_failed, auth.org_setup, auth.token_refreshed
SSOsso.connection_created, sso.connection_deleted, sso.enforcement_changed
Useruser.profile_updated, user.updated_by_admin, user.active_org_switched
Billingbilling.subscription_changed, billing.subscription_status_changed, billing.subscription_event_failed
Operatoradmin.plan_changed, admin.limits_overridden, admin.status_changed, admin.trial_changed, admin.org_disabled, admin.org_enabled, admin.user_disabled, admin.user_enabled, admin.billing_event_replayed, admin.access_request_reviewed, admin.access_request_approved

The admin.* actions come from the operator panel, not from anything a member of your organisation can do. They appear in your log when an operator acts on your organisation, which is the point.

Environment changes are not audited. Creating, renaming, promoting or deleting an environment writes no entry. Neither do alert rules or moderation rules. Only the actions in the table above are captured. If your compliance story depends on covering one of those, say so, do not infer it from this page.

Entry shape

{
  "id": "8f3c1a90-2b7e-4d51-9a06-7c3e1f5b2d84",
  "organizationId": "org_01HZX9K2QF",
  "actorId": "usr_01HZX9K3PA",
  "action": "api_key.created",
  "resourceType": "api_key",
  "resourceId": "ak_live_9f2c…",
  "metadata": {
    "name": "prod deploy",
    "environmentId": "env_01HZX9K4TC"
  },
  "ipAddress": "203.0.113.17",
  "actor": {
    "id": "usr_01HZX9K3PA",
    "first_name": "Sayan",
    "last_name": "Biswas",
    "email": "sayan@example.com",
    "username": "sayan"
  },
  "createdAt": "2026-08-21T18:03:11.742Z"
}

Every identifier is a string. id is the entry’s UUID.

actorId and actor are null when no authenticated caller was responsible, a failed sign-in has no actor, and neither does a system-triggered change. actor is resolved by looking the user up at read time, so a removed user’s entries keep their actorId but come back with actor: null.

metadata is action-specific and unvalidated. Do not build a schema on it; read the fields you need defensively.

Querying

GET /audit-logs?page=1&limit=50
Authorization: Bearer <jwt>

Requires the Admin role in the active organisation; anyone else gets 403. The organisation comes from the caller’s session, not from a parameter, you cannot read another organisation’s log by asking nicely.

Filters

ParameterTypeDefaultNotes
pagenumber1
limitnumber50
actionstringExact match, e.g. api_key.created.
resourceTypestringExact match, e.g. api_key, channel.
actorIdstringThe user who performed the action.
fromISO 8601Inclusive lower bound on createdAt.
toISO 8601Inclusive upper bound on createdAt.

from and to are the cheap filters: the log is stored partitioned by organisation and sorted by createdAt, so a bounded range is a direct range read. action, resourceType and actorId are applied as a post-filter over that range, so narrowing the window first is what keeps a query fast.

Response

{
  "data": [ /* newest first */ ],
  "meta": {
    "total": 1284,
    "page": 1,
    "limit": 50,
    "totalPages": 26
  }
}

meta.total counts the entries the query has walked so far, not the size of the log. Asking for page 1 walks roughly one page’s worth and reports that as the total. The number converges on the truth as you page deeper, and is exact only once you have reached the end. Use it to decide “is there more”, not to render “1,284 events”.

Exporting a window

Page until a request returns fewer than limit rows, rather than trusting totalPages:

page=1
while :; do
  n=$(curl -sS \
    "https://api.axonpush.xyz/audit-logs?from=2026-07-01&to=2026-08-01&limit=200&page=$page" \
    -H "Authorization: Bearer $AXONPUSH_JWT" \
    | tee "audit-$page.json" | jq '.data | length')
  [ "$n" -lt 200 ] && break
  page=$((page + 1))
done

Guarantees

  • Append-only. There is no API to edit or delete an entry, and none of the write paths update one.
  • Organisation-scoped. The read path derives the organisation from the caller. There is no cross-organisation query.
  • Best-effort write. A failure to record is logged server-side and swallowed; the action it describes still succeeds. This keeps a logging outage from becoming an availability outage, and it means the log is not a transaction record.
  • No expiry. Unlike events and traces, audit entries carry no TTL and are not aged out. The log grows for the life of the organisation.