Audit log
Requires a license with the
auditfeature. Streaming the log to an outside sink also needsaudit-pro, which a license withauditdoes 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.
How it works
Section titled “How it works”- 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
sequenceand achain_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.resetorai.settings.update. Resource types are plain names, such asuser.
Turn it on
Section titled “Turn it on”- Install a license that includes
audit. See licensing and tiers. - Set
LYEVE_AUDIT_HMAC_KEYto 64 hex characters. Generate one withopenssl 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. - 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.
Try it
Section titled “Try it”This needs a license with audit. You need an admin token in TOKEN. The
quickstart shows how to get one.
-
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} -
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" -
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,ts1170a17f-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.045612Z5ab83683-a564-4900-8115-a68dd5c27dc0,3e7cc859-1207-4c7c-9af2-363fa5079e0b,schema.delete,schema,post,****:****:****:35590,curl/8.18.0,2026-10-01T12:09:41.813285Z -
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 }
Search the log
Section titled “Search the log”GET /api/admin/audit-log needs an admin. An
admin token can call it when it holds the
audit:read grant.
| Parameter | Meaning | Default |
|---|---|---|
actor | User id of the actor. | all |
action | Exact action name. | all |
resource_type | Exact resource type. | all |
resource_id | Exact resource id. | all |
from | RFC 3339, inclusive. | none |
to | RFC 3339, exclusive. Must be after from. | none |
tenant_id | One tenant. Super admin only, ignored for anyone else. | your tenant, or every tenant for a super admin |
limit | Page size, at most 500. | 50 |
offset | Rows 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.
Export the log
Section titled “Export the log”| Request | Who |
|---|---|
POST /api/admin/audit-log/export | An admin. Exports their own tenant. |
POST /api/admin/audit-log/export-all | A super admin. Exports every tenant. |
Both take the same body. Every field is optional except format.
| Field | Meaning | Default |
|---|---|---|
format | json or csv. CSV has a header row. | |
from | RFC 3339, inclusive. | none |
to | RFC 3339, exclusive. Cannot be before from. | none |
action, resource_type | Exact filters. | all |
limit | Rows. 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.
Send an export to S3
Section titled “Send an export to S3”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 akeyyou send is ignored. Onexport-allthekeyyou 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.
regionin the request wins overAWS_REGION.
Prune old entries
Section titled “Prune old entries”POST /api/admin/audit-log/prune deletes entries older than a cutoff in the
caller's tenant. It needs a super admin.
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 }Retention policies and legal holds
Section titled “Retention policies and legal holds”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:
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:
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:
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.csvRetention and hold routes
| Method | Path | Who | Purpose |
|---|---|---|---|
GET | /api/admin/retention-policies | Admin | List policies. |
POST | /api/admin/retention-policies | Super admin | Create a policy. |
PUT | /api/admin/retention-policies/{id} | Super admin | Update a policy. |
DELETE | /api/admin/retention-policies/{id} | Super admin | Delete a policy. |
GET | /api/admin/legal-holds | Admin | List holds. |
POST | /api/admin/legal-holds | Super admin | Create a hold. |
DELETE | /api/admin/legal-holds/{id} | Super admin | Remove a hold. |
POST | /api/admin/retention/enforce | Super admin | Run enforcement now. |
POST | /api/admin/retention/compliance-export | Super admin | Export what a hold preserves. |
Stream the log to an outside sink
Section titled “Stream the log to an outside sink”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.
Record and read from a flow
Section titled “Record and read from a flow”With flow-pro, flows can use two audit nodes:
audit.recordwrites one entry to the tenant's log. The actor is the flow unless you give a user id. A test run writes nothing.audit.queryreads a page of the tenant's log, newest first, with the same filters asGET /api/admin/audit-logand the same masking.
Settings
Section titled “Settings”| Variable | What it does | Default |
|---|---|---|
LYEVE_AUDIT_HMAC_KEY | Key for the hash chain, exactly 64 hex characters. | Required in production |
RETENTION_ENFORCE_INTERVAL | How often retention policies are enforced. | 24h |
If you set LYEVE_PLUGINS to choose which features start, include audit in
it.
Routes
Section titled “Routes”| Method | Path | Who | Purpose |
|---|---|---|---|
GET | /api/admin/audit-log | Admin | Search the log. |
POST | /api/admin/audit-log/export | Admin | Export your tenant. |
POST | /api/admin/audit-log/export-all | Super admin | Export every tenant. |
POST | /api/admin/audit-log/prune | Super admin | Delete entries before a cutoff. |
Errors
Section titled “Errors”| Status | Message | Cause |
|---|---|---|
400 | invalid filter parameters | A list parameter is malformed, or to is not after from. |
400 | format must be 'json' or 'csv' | Export or compliance export with another format. |
400 | 'to' must be after 'from' | Export range is reversed. |
400 | limit must be >= 0 | Export with a negative limit. |
400 | older_than must be RFC 3339 (e.g. 2024-01-01T00:00:00Z) | Prune cutoff is not a timestamp. |
400 | name is required | Legal hold without a name. |
400 | legal_hold_id is required | Compliance export without a hold. |
402 | payment_required | The license lapsed while the instance ran. |
403 | tenant context required for export | Tenant export from a token with no tenant. |
403 | insufficient permissions | A tenant admin called a super admin route, such as prune or export-all. |
422 | s3 bucket is not a valid bucket name | The bucket name breaks S3 naming rules. |
503 | failed to load audit log, s3: upload failed | The database or S3 did not answer. Retry. |
Related
Section titled “Related”- Logs: what the instance logged while serving requests.
- Admin tokens: an
audit:readtoken for a script or a SIEM. - Analytics: daily active users, counted from this log.
- Secure your instance: the hardening steps, including the audit key.