Audit log API
Use this API to find out who did what. It reads the engine’s records (the history of everything that happened), which already name the acting principal: the person or client that made the request. The Audit area of TinyConductor Console uses the same API.
Access
Section titled “Access”- Requires the permission
audit:read(the built-inoperatorandadminroles have it). Without it:403. - The tenant always comes from the caller’s credential. It is never a query parameter. In Cluster mode, the database’s row-level security and an explicit tenant filter both limit the read to that tenant.
- All three modes (Embedded, Bundled and Cluster) serve it the same way,
with the same accepted requests and refusals. No mode answers
mode-not-supported.
Query parameters
Section titled “Query parameters”All parameters are optional. An unknown or repeated parameter is 400.
| Parameter | Meaning |
|---|---|
category | RECORD (who did what), ACCESS (refused requests) or IDENTITY (changes to users, groups, roles, permissions, API clients, mapping rules and tenants); all three when absent |
subject | Exact acting subject, e.g. alice. A person is named as in models: alice for the default identity provider, alice@corp for another one (Who a person is) |
principalKind | user, service, client, static or anonymous (see below) |
intent | Record intent, e.g. ASSIGNED, COMPLETED, CREATED, CANCELED |
action | Command action carried by the payload, e.g. claim, assign, unassign, complete (user-task records), or the change of an IDENTITY entry, e.g. client.secret.rotate |
valueType | Record family, e.g. USER_TASK, PROCESS_INSTANCE_CREATION, PROCESS_INSTANCE_MODIFICATION, INCIDENT, DEPLOYMENT |
processInstanceKey | Positive process instance key |
from | Inclusive lower bound, RFC 3339 (2026-09-23T10:00:00Z) |
to | Exclusive upper bound, RFC 3339. from must be earlier than to |
pageSize | 1–200; 0 or absent means 50; above 200 is 400 |
cursor | The cursor of the previous page |
Refusal details are the same in every mode, for example
pageSize exceeds the maximum of 200,
unknown audit query parameter \tenantId`, invalid cursor`.
Response
Section titled “Response”{ "items": [ { "timestamp": 1790158860000000, "partitionId": 1, "position": 42, "subject": "lead", "principalKind": "user", "tenantId": "tenant-a", "recordType": "EVENT", "valueType": "USER_TASK", "intent": "ASSIGNED", "action": "claim", "key": "2251799813685301", "processInstanceKey": "2251799813685290", "correlationId": "…" } ], "cursor": "1790158860000000:1:41"}- Entries are newest first, ordered by
(timestamp, partitionId, position). timestampis epoch microseconds. Entity keys are strings.action,correlationId,rejectionTypeandrejectionReasonare present only when the record has them. A rejected command appears withrecordType: "COMMAND_REJECTION".cursorisnullon the last page.- A user-task command produces a pair of records (for example
ASSIGNINGthenASSIGNED), both with the actor. Filter byintent=ASSIGNED(orCOMPLETED, …) for one line per action. - For a claim made on someone else’s behalf,
subjectis who acted. The new assignee is in the task, not in the audit line.
What is and is not in the log
Section titled “What is and is not in the log”-
In: every record with a principal: deployments, instance creation, cancellation, modification and migration, variable updates, incident resolution, user-task lifecycle, and the engine’s own service principal (for example
bpm-runtimecreating a user task). -
Also in, as
ACCESSentries: every request refused for wrong, expired or insufficient credentials:401,403, and400for a missing tenant selection, including refused token requests. A request that sends no credential at all (no token and no session, as when a browser opens the sign-in page) is refused with401but not recorded. Bundled and Cluster mode store them, and theIDENTITYentries below, in their PostgreSQL database; Embedded mode keeps the most recent ones in memory.- An access entry has
"category": "ACCESS",partitionId: -1,status,method,route, and the reason inrejectionReason. principalKindnames the kind of credential that was refused:user(a person: a session or a sign-in),client(an API client’s token or its client credentials),static(a static token),service(a service identity from your identity provider), oranonymouswhen the credential named nobody the engine knows.subjectis absent when no credential was recognized.- It has no
recordType,valueType,intent,actionorprocessInstanceKey, so those filters select records only.
See Identity and access.
- An access entry has
{ "category": "ACCESS", "timestamp": 1790158861000000, "partitionId": -1, "position": 17, "subject": "billing-worker", "principalKind": "client", "tenantId": "tenant-a", "status": 403, "method": "POST", "route": "/v2/process-instances", "key": "17", "rejectionReason": "principal is not entitled to this action on this resource"}Record entries carry "category": "RECORD". Their principalKind is
user or service, as the record stores it; an API client acting in a
record is a service.
- Also in, as
IDENTITYentries: every change made through the administration API (/v2/users,/v2/groups,/v2/roles,/v2/mapping-rules,/v2/authorizations,/v2/clients,/v2/tenants), whether it was made or refused: creating, changing and deleting users, groups, roles, permissions, API clients (including a new secret and a revocation) and mapping rules; adding and removing members; and creating, changing (name, limits, retention, administrators), suspending, reactivating, releasing from quarantine, deleting and moving tenants. Searches and reads are not recorded.- An identity entry has
"category": "IDENTITY",partitionId: -2, who asked (subject,principalKind),action(for exampleuser.create,group.member.add,client.secret.rotate,tenant.suspend),targets(the ids it changed: from the path, the id a create names, a new permission’sauthorizationKey, a move’smoveId),result(DONE, orREFUSEDwith the answer’sstatusand the reason inrejectionReason),method,routeandsummary. summaryis what the request asked for. Only fields that never carry a secret keep their values (names, descriptions, ids, permission types, claim names and values, expiry, limits, retention and administrators); every other field is listed with the value(not recorded). A client secret is only ever in an answer and is never recorded; neither are passwords or password hashes.- An entry is filed under the tenant of the person or client who made the change. The starting tenant’s operators manage the other tenants, so tenant changes are in the starting tenant’s audit log.
- A request refused before it reaches the administration API (no
credential, or no permission for the route) is an
ACCESSentry instead.
- An identity entry has
{ "category": "IDENTITY", "timestamp": 1790158862000000, "partitionId": -2, "position": 5, "subject": "ada", "principalKind": "user", "tenantId": "tenant-a", "action": "client.create", "targets": {"clientId": "billing-worker"}, "result": "DONE", "status": 201, "method": "POST", "route": "/v2/clients", "summary": {"clientId": "billing-worker", "name": "Billing worker"}, "key": "5"}The record entries of a finished instance are removed with the instance when
the tenant’s history retention has passed (30 days after it ended by
default; see
How long history is kept).
Record entries that belong to no instance (a published message, a signal, a
refused request) are removed once they are past the same retention;
deployments are not affected. ACCESS and IDENTITY entries are kept for
CONDUCTOR_AUTH_AUDIT_RETENTION_DAYS days (30 by default). Repeated identical
refusals are recorded five times a minute and then counted in one summary
entry (see
Refusals and response headers).
Checking that the log was not changed — GET /v1/audit-log/integrity
Section titled “Checking that the log was not changed — GET /v1/audit-log/integrity”In Bundled and Cluster mode the ACCESS and IDENTITY entries are stored in
PostgreSQL in a way that shows later changes. Each tenant’s entries of each
kind form a chain: every entry is stored with its number in the chain and a
fingerprint (a SHA-256 hash) of its own content together with the
fingerprint of the entry before it. Changing an entry, removing one, or
removing the newest ones breaks the chain at that point.
GET /v1/audit-log/integrity checks your tenant’s chains and needs the same
audit:read permission as the log itself:
{ "durable": true, "intact": false, "chains": [ { "category": "ACCESS", "entries": 1204, "firstSequence": 37, "lastSequence": 1240, "headSequence": 1240, "headHash": "5f0c…", "intact": true, "broken": null }, { "category": "IDENTITY", "entries": 58, "firstSequence": 1, "lastSequence": 58, "headSequence": 58, "headHash": "a91e…", "intact": false, "broken": { "sequence": 12, "problem": "CHANGED", "detail": "entry 12 was changed after it was stored" } } ]}brokennames the first place where the chain no longer holds:CHANGED(the entry’s content was changed),UNLINKED(the entry no longer follows the one before it),MISSING(entries in the middle were removed) orHEAD(the newest entries were removed).- Removing old entries is what retention does, so a chain that starts
later than entry 1 (
firstSequence) is normal. Newest entries are only expected to be missing when they are older than the retention. - The check reads every stored entry of your tenant, so call it when you audit, not on every page load.
- Embedded mode keeps its audit in memory only and answers
"durable": falsewith no chains.
What this protects against: someone who changes or deletes entries
directly in the database, for example with the engine’s database login,
without going through the engine. What it does not protect against:
someone who also rewrites every fingerprint after the changed entry; the
fingerprints are not secret, so anyone who can write the tables can do
that. To detect that too, keep each chain’s headSequence and headHash
outside the installation from time to time (in a ticket or your log
store). Later, ask for the fingerprint stored at that position with
GET /v1/audit-log/integrity?sequence=1240: each chain then also answers
hashAtSequence. If it differs from the headHash you kept, the history
up to that point was rewritten. Record entries (RECORD) are part of the
engine’s own log and are not in these chains.