Skip to content

Audit log

Requires a license with the audit feature. Streaming the log to an outside sink also needs audit-pro, which a license with audit does not include. See pricing.

The audit log records the actions admins and the instance take: who acted, what they changed, when, and from which address. You can search it, export it as JSON or CSV, send an export to an S3 bucket, and decide how long entries are kept.

  • Nothing in the API edits an entry, except a privacy erasure, which anonymizes the subject's personal data in their entries. Entries leave the log only through a super admin prune, a retention policy or the deletion of their tenant.
  • Each entry carries a sequence and a chain_hash, a keyed hash of the entry and the hash stored on the entry before it.
  • A tenant admin sees only their own tenant, with IP addresses and user agents masked. A super admin sees full values and every tenant.
  • Actions are named by area and verb, such as mfa.admin.reset or ai.settings.update. Resource types are plain names, such as user.
  1. Install a license that includes audit. See licensing and tiers.
  2. Set LYEVE_AUDIT_HMAC_KEY to 64 hex characters. Generate one with openssl rand -hex 32. A production instance refuses to start without it. Outside production, a missing key logs a warning and the chain is not protected by a secret.
  3. Open Insight > Audit log in the admin console.

Keep LYEVE_AUDIT_HMAC_KEY stable. Changing it breaks the chain for entries written before the change. Without the license, the routes are not served and answer 404.

This needs a license with audit. You need an admin token in TOKEN. The quickstart shows how to get one.

  1. Read the newest entry:

    Terminal window
    curl "http://localhost:3001/api/admin/audit-log?limit=1" \
    -H "Authorization: Bearer $TOKEN"
    {
    "data": [
    {
    "id": "1170a17f-5e00-4193-87ab-49d20fdef6c6",
    "sequence": 92,
    "tenant_id": "default",
    "user_id": "3e7cc859-1207-4c7c-9af2-363fa5079e0b",
    "action": "ai.settings.update",
    "resource_type": "ai_settings",
    "resource_id": "default",
    "ip": "[::1]:35606",
    "user_agent": "curl/8.18.0",
    "created_at": "2026-10-01T12:09:44.045612Z",
    "chain_hash": "8fd607e5a35021e3667cea11fb867ed75eac7802270ea75a3444ffb3f9ec9e34"
    }
    ],
    "total": 92,
    "limit": 1,
    "offset": 0
    }
  2. Narrow it to actions on users:

    Terminal window
    curl "http://localhost:3001/api/admin/audit-log?resource_type=user&limit=10" \
    -H "Authorization: Bearer $TOKEN"
  3. Download two rows as CSV:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/audit-log/export \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"format": "csv", "limit": 2}'
    id,user_id,action,resource_type,resource_id,ip,user_agent,ts
    1170a17f-5e00-4193-87ab-49d20fdef6c6,3e7cc859-1207-4c7c-9af2-363fa5079e0b,ai.settings.update,ai_settings,default,****:****:****:35606,curl/8.18.0,2026-10-01T12:09:44.045612Z
    5ab83683-a564-4900-8115-a68dd5c27dc0,3e7cc859-1207-4c7c-9af2-363fa5079e0b,schema.delete,schema,post,****:****:****:35590,curl/8.18.0,2026-10-01T12:09:41.813285Z
  4. List the retention policies. A new install has none:

    Terminal window
    curl http://localhost:3001/api/admin/retention-policies \
    -H "Authorization: Bearer $TOKEN"
    { "data": [], "limit": 50, "offset": 0, "total_count": 0 }

GET /api/admin/audit-log needs an admin. An admin token can call it when it holds the audit:read grant.

ParameterMeaningDefault
actorUser id of the actor.all
actionExact action name.all
resource_typeExact resource type.all
resource_idExact resource id.all
fromRFC 3339, inclusive.none
toRFC 3339, exclusive. Must be after from.none
tenant_idOne tenant. Super admin only, ignored for anyone else.your tenant, or every tenant for a super admin
limitPage size, at most 500.50
offsetRows to skip.0

The list writes out pending entries before it reads, so an action you just took is in the result. If some were still queued, the answer carries "pending_writes": true and total is short.

RequestWho
POST /api/admin/audit-log/exportAn admin. Exports their own tenant.
POST /api/admin/audit-log/export-allA super admin. Exports every tenant.

Both take the same body. Every field is optional except format.

FieldMeaningDefault
formatjson or csv. CSV has a header row.
fromRFC 3339, inclusive.none
toRFC 3339, exclusive. Cannot be before from.none
action, resource_typeExact filters.all
limitRows. 0 means the default. Values above 1,000,000 are capped.100000
s3{"bucket", "key", "region"}. Uploads the export instead of returning it.none

IP addresses are masked in both exports, and user agents are cut to the browser or client name and version. A compliance export of a legal hold (below) keeps full values.

A download starts streaming before every row is read. If a row fails midway, the file ends early and the status is still 200. Compare the row count with what you expect.

Add "s3": {"bucket": "my-audit", "region": "us-east-1"} to the body. The answer is:

{ "rows_written": 5210, "object_key": "audit-exports/acme/2026/10/01/audit-export.csv", "bucket": "my-audit", "elapsed": "1.84s" }
  • Objects are encrypted at rest with S3 server-side encryption (AES256).
  • On the tenant export the object key is always under audit-exports/<tenant>/<YYYY/MM/DD>/, and a key you send is ignored. On export-all the key you send is used. It is required and cannot contain ... The format extension is added when the key lacks it.
  • Credentials come from the standard AWS sources: environment variables, the shared config file, or the instance role. region in the request wins over AWS_REGION.

POST /api/admin/audit-log/prune deletes entries older than a cutoff in the caller's tenant. It needs a super admin.

Terminal window
curl -X POST http://localhost:3001/api/admin/audit-log/prune \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"older_than": "2025-01-01T00:00:00Z"}'
{ "deleted": 3812 }

A retention policy says how many days entries of an action and resource type are kept, and whether they are archived to your storage backend before they are deleted. A legal hold keeps matching entries past their retention window until the hold is removed.

A license with audit includes these routes. If the license lapses while the instance runs, they answer 402 and nothing is enforced. A license change applies without a restart. A privacy erasure of a subject that an active hold names in filter_resource_id or filter_actor erases nothing and reports the hold instead, whatever the license says.

Enforcement runs once at start and then every 24 hours. Set RETENTION_ENFORCE_INTERVAL to a duration such as 6h to change that.

Create a policy that keeps user entries for 180 days and archives them first:

Terminal window
curl -X POST http://localhost:3001/api/admin/retention-policies \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"event_type": "", "resource_type": "user", "retention_days": 180, "archival_enabled": true, "archival_path_prefix": "audit-archive/"}'

event_type is the action. An empty event_type or resource_type matches everything. retention_days defaults to 365 when you leave it out. An explicit 0 deletes matching entries on the next run.

Create a hold:

Terminal window
curl -X POST http://localhost:3001/api/admin/legal-holds \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Case 2026-114", "filter_resource_type": "user", "filter_from": "2026-01-01T00:00:00Z"}'

Holds filter on filter_action, filter_resource_type, filter_resource_id, filter_actor, filter_from, filter_to and filter_tenant_id. name is required. A removed hold keeps protecting its entries for 30 days.

Run enforcement now with POST /api/admin/retention/enforce. Send {"cleanup_only": true} to delete without archiving. The answer lists each policy with entries_archived, entries_deleted and elapsed.

Export what a hold preserves:

Terminal window
curl -X POST http://localhost:3001/api/admin/retention/compliance-export \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"legal_hold_id": "7e2a4b9c-1d05-4e68-b3f7-90a6c2d8e1f5", "format": "csv"}' -o hold.csv
Retention and hold routes
MethodPathWhoPurpose
GET/api/admin/retention-policiesAdminList policies.
POST/api/admin/retention-policiesSuper adminCreate a policy.
PUT/api/admin/retention-policies/{id}Super adminUpdate a policy.
DELETE/api/admin/retention-policies/{id}Super adminDelete a policy.
GET/api/admin/legal-holdsAdminList holds.
POST/api/admin/legal-holdsSuper adminCreate a hold.
DELETE/api/admin/legal-holds/{id}Super adminRemove a hold.
POST/api/admin/retention/enforceSuper adminRun enforcement now.
POST/api/admin/retention/compliance-exportSuper adminExport what a hold preserves.

With audit-pro on the same license as audit, a tenant streams each new entry to Splunk, Datadog or an HTTPS endpoint of its own, in order and about 10 to 20 seconds after it is written. Streams already set up keep shipping after audit-pro lapses. See Audit log streaming.

With flow-pro, flows can use two audit nodes:

  • audit.record writes one entry to the tenant's log. The actor is the flow unless you give a user id. A test run writes nothing.
  • audit.query reads a page of the tenant's log, newest first, with the same filters as GET /api/admin/audit-log and the same masking.
VariableWhat it doesDefault
LYEVE_AUDIT_HMAC_KEYKey for the hash chain, exactly 64 hex characters.Required in production
RETENTION_ENFORCE_INTERVALHow often retention policies are enforced.24h

If you set LYEVE_PLUGINS to choose which features start, include audit in it.

MethodPathWhoPurpose
GET/api/admin/audit-logAdminSearch the log.
POST/api/admin/audit-log/exportAdminExport your tenant.
POST/api/admin/audit-log/export-allSuper adminExport every tenant.
POST/api/admin/audit-log/pruneSuper adminDelete entries before a cutoff.
StatusMessageCause
400invalid filter parametersA list parameter is malformed, or to is not after from.
400format must be 'json' or 'csv'Export or compliance export with another format.
400'to' must be after 'from'Export range is reversed.
400limit must be >= 0Export with a negative limit.
400older_than must be RFC 3339 (e.g. 2024-01-01T00:00:00Z)Prune cutoff is not a timestamp.
400name is requiredLegal hold without a name.
400legal_hold_id is requiredCompliance export without a hold.
402payment_requiredThe license lapsed while the instance ran.
403tenant context required for exportTenant export from a token with no tenant.
403insufficient permissionsA tenant admin called a super admin route, such as prune or export-all.
422s3 bucket is not a valid bucket nameThe bucket name breaks S3 naming rules.
503failed to load audit log, s3: upload failedThe database or S3 did not answer. Retry.
  • Logs: what the instance logged while serving requests.
  • Admin tokens: an audit:read token for a script or a SIEM.
  • Analytics: daily active users, counted from this log.
  • Secure your instance: the hardening steps, including the audit key.