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.
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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
from | query | no | string | ISO 8601 instant, e.g. 2026-08-18T00:00:00Z. |
to | query | no | string | |
operations | query | no | string | Comma-separated non-negative integer operation ids. Get them from GET /api/audit/operations, which returns the ids actually present in this log. |
principalType | query | no | string | Matches a row's actor OR its object. Five accepted values; the 400 detail names them. |
principalName | query | no | string | A name PREFIX. |
outcome | query | no | string (succeeded, failed) | |
limit | query | no | integer | Must be one of the offered page sizes, echoed in paging.pageSizes. Refused, never coerced. |
offset | query | no | integer |
Request
No request body.
Responses
200
A page of rows.
| Field | Type | Required | Description |
|---|---|---|---|
rows | array of object | yes | |
paging | object | yes | |
nameFilterTooBroad | boolean | no | |
nameMatchCount | object | no | |
auditLogFilter | object | no | Whether the key server is dropping audit rows before they are written. Null when there is nothing to report. |
rows[] object
| Field | Type | Required | Description |
|---|---|---|---|
rowId | object | no | |
time | string | no | |
operationId | integer | no | |
operationName | string or null | no | |
status | object | no | |
outcome | string | no | |
actor | AuditPrincipal | no | One side of an audit row -- the actor, or the object it acted on. |
object | AuditPrincipal | no | One side of an audit row -- the actor, or the object it acted on. |
detail | object | no |
paging object
| Field | Type | Required | Description |
|---|---|---|---|
offset | integer | no | |
limit | integer | no | |
returned | integer | no | |
hasMore | boolean | no | The answer to "is there more". There is no total. |
pageSizes | array of integer | no |
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 code — detail is written for a human debugging the call and its wording is not part of the contract. See the error model.
| Status | Meaning |
|---|---|
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. |