Skip to main content

GET /api/audit

Read a page of the audit log.

Returns a page of audit rows.

This is the one family on this surface with no key server in its request path. It reads PostgreSQL directly through a reader the operator configures, so its failure modes are the database's rather than the key server's.

Paging is OFFSET-BASED and there is no total. The answer to "is there more" is paging.hasMore, not a row count. limit must be one of the offered page sizes and is REFUSED rather than coerced to a default: a page size silently replaced looks like it was honoured.

outcome is spelled succeeded or failed -- not ok/error. The underlying column is an ERROR FLAG whose zero means SUCCEEDED, which is the opposite of how it reads, so the two explicit spellings are the only ones accepted.

principalType matches a row's ACTOR or its OBJECT. There are five accepted values and group is one of them, because group principals appear on audit rows almost entirely as the object.

from and to are refused here if they are not ISO 8601 instants, rather than passed through: the reader parses them deep inside the query and throws, which would surface as a database failure and blame the database for a query-string typo.

WARNING: 503 and 502 here are NOT empty results. AUDIT_READER_UNAVAILABLE means no reader is configured and no query was run; AUDIT_DB_UNAVAILABLE means no rows were read. Treating either as "no activity" would be a wrong answer rather than an error.

Authentication

Send the session id as Authorization: Bearer <session id>, and an RFC 9449 DPoP proof in the DPoP header if your session is bound to a key. The two are required together, not as alternatives — see Authenticating and Proving you hold your key.

This surface sets no cookie

There is no session cookie on any response and none is read on any request. Do not set credentials: "include" on a browser fetch.

Parameters

NameInRequiredTypeDescription
fromquerynostringISO 8601 instant, e.g. 2026-08-18T00:00:00Z.
toquerynostring
operationsquerynostringComma-separated non-negative integer operation ids. Get them from GET /api/audit/operations, which returns the ids actually present in this log.
principalTypequerynostringMatches a row's actor OR its object. Five accepted values; the 400 detail names them.
principalNamequerynostringA name PREFIX.
outcomequerynostring (succeeded, failed)
limitquerynointegerMust be one of the offered page sizes, echoed in paging.pageSizes. Refused, never coerced.
offsetquerynointeger

Request

No request body.

Responses

200

A page of rows.

FieldTypeRequiredDescription
rowsarray of objectyes
pagingobjectyes
nameFilterTooBroadbooleanno
nameMatchCountobjectno
auditLogFilterobjectnoWhether the key server is dropping audit rows before they are written. Null when there is nothing to report.

rows[] object

FieldTypeRequiredDescription
rowIdobjectno
timestringno
operationIdintegerno
operationNamestring or nullno
statusobjectno
outcomestringno
actorAuditPrincipalnoOne side of an audit row -- the actor, or the object it acted on.
objectAuditPrincipalnoOne side of an audit row -- the actor, or the object it acted on.
detailobjectno

paging object

FieldTypeRequiredDescription
offsetintegerno
limitintegerno
returnedintegerno
hasMorebooleannoThe answer to "is there more". There is no total.
pageSizesarray of integerno

Example

{
"rows": [
{
"rowId": {},
"time": "...",
"operationId": 0,
"operationName": "...",
"status": {},
"outcome": "...",
"actor": {
"system": {},
"principal": {},
"name": "...",
"type": "...",
"label": "..."
},
"object": {
"system": {},
"principal": {},
"name": "...",
"type": "...",
"label": "..."
},
"detail": {}
}
],
"paging": {
"offset": 0,
"limit": 0,
"returned": 0,
"hasMore": true,
"pageSizes": [
0
]
},
"nameFilterTooBroad": true,
"nameMatchCount": {},
"auditLogFilter": {}
}

400

The request was refused before anything was sent to the key server.

Most 400s on this surface carry code: BAD_REQUEST and a detail naming the field and saying what the key server would otherwise have done with it. A large share of them exist because the value would have been ACCEPTED downstream and quietly meant something else -- an empty string that reaches the wire as "mint a new key", a zero-length identifier that names no object, a numeric flag word that clears bits the caller did not name.

401

No live session, or a DPoP proof that did not verify.

SESSION_NOT_FOUND -- the credential names no session this Gateway holds. SESSION_EXPIRED -- it aged out. Both carry reauth: true; obtain a new credential rather than retrying.

PROOF_* -- the sender constraint refused. See the DPoP section of this document for the full table; PROOF_MALFORMED is a 400 and PROOF_REPLAY_CACHE_FULL a 503, and the rest are here.

SCOPED_CREDENTIAL_EXPIRED -- a scoped browser credential passed its fixed lifetime. Use does not extend it: an idle timeout would be refreshed by exactly the traffic a stolen handle produces.

502

AUDIT_DB_UNAVAILABLE -- the audit database did not answer. Not an empty result. Retryable. The failure sentence is withheld from this response because it can carry a database name, address, SQL or a role name; it is in the Gateway's server log.

503

AUDIT_READER_UNAVAILABLE -- this Gateway has no audit reader, so no query was run. Not an empty result. The operator must configure the database connection and restart the process. Not retryable.

Error handling

Every failure answers JSON carrying at least code and detail. Match on codedetail is written for a human debugging the call and its wording is not part of the contract. See the error model.

StatusMeaning
400The request was refused before anything was sent to the key server. Most 400s on this surface carry code: BAD_REQUEST and a detail naming the field and saying what the key server would otherwise have done with it. A large share of them exist because the value would have been ACCEPTED downstream and quietly meant something else -- an empty string that reaches the wire as "mint a new key", a zero-length identifier that names no object, a numeric flag word that clears bits the caller did not name.
401No live session, or a DPoP proof that did not verify. SESSION_NOT_FOUND -- the credential names no session this Gateway holds. SESSION_EXPIRED -- it aged out. Both carry reauth: true; obtain a new credential rather than retrying. PROOF_* -- the sender constraint refused. See the DPoP section of this document for the full table; PROOF_MALFORMED is a 400 and PROOF_REPLAY_CACHE_FULL a 503, and the rest are here. SCOPED_CREDENTIAL_EXPIRED -- a scoped browser credential passed its fixed lifetime. Use does not extend it: an idle timeout would be refreshed by exactly the traffic a stolen handle produces.
502AUDIT_DB_UNAVAILABLE -- the audit database did not answer. Not an empty result. Retryable. The failure sentence is withheld from this response because it can carry a database name, address, SQL or a role name; it is in the Gateway's server log.
503AUDIT_READER_UNAVAILABLE -- this Gateway has no audit reader, so no query was run. Not an empty result. The operator must configure the database connection and restart the process. Not retryable.