GET /api/v1/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/v1/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) | |
reason | query | no | string (none, aclDenied, roleDenied, marshallingError, ldapError, systemError, unclassifiedFailure, credentialRefused, operationUnavailable, buildAttestationRefused, objectNotFound, decryptFailed) | The failure CAUSE, by key -- independent of outcome. An unknown key is refused with a 400 naming the accepted set. |
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 | |
reason | integer | no | The stored failure-cause code (AuditReason); 0 on a success. |
reasonName | string | no | The cause's display name; Unknown reason (N) for a code the reader does not list. |
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": "...",
"reason": 0,
"reasonName": "...",
"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.
INVALID_ARGUMENT is the one 400 that DID reach the key server: the key server itself
refused the argument (ERR_INVALID_ARGUMENT), and detail is its own sentence. On
POST /api/v1/certificates/enroll that is a certificate that cannot authenticate --
certificate not permitted for authentication: keyUsage is present without digitalSignature (...), ... extendedKeyUsage is present without clientAuth (...), or
certificate expired at enrolment: notAfter <UTC> ... -- and a multi-certificate body
that carries one such leaf persists nothing.
KEY_UNWRAP_FAILED reached the key server too, on POST /api/v1/keys/import with format
WRAPPED: the wrapped key did not unwrap under the key server's trust certificate --
wrapped to another key, by another method, or damaged. Nothing was stored for that key.
401
No live session, a DPoP proof that did not verify, or a certificate session whose certificate the key server now refuses.
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.
CERTIFICATE_REVOKED -- on a session established by certificate login, the key server
re-checks the certificate that session was established with on its operations, and it
is now named by revocation evidence. detail is the key server's sentence. The
Gateway answers this once and then ENDS everything that service holds or minted here --
its sessions, the delegated sessions and the scoped credentials it minted -- so a later
operation on any of them is SESSION_NOT_FOUND. Log in with a different certificate.
SSO_SESSION_EXPIRED -- a single-sign-on session (POST /api/v1/session/oidc) reached
its lifetime, or was idle past its binding's idle bound; it has been ended. Sign in again.
SENDER_KEY_RETIRED -- the session's registered sender key was rotated out and its
overlap has passed (see "The registered service's credential lifecycle"); the session is
closed with this answer. SERVICE_REGISTRATION_ENDED -- the registration a service
credential was issued under was removed or narrowed on this Gateway; it has ended. Both
are final for that credential.
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. Or CAPACITY_UNAVAILABLE (scope: process, Retry-After) -- this Gateway is at its request ceiling (see "Rate and capacity limits").
413
PAYLOAD_TOO_LARGE (retryable: false) -- the request body is larger than this
deployment accepts. It was refused before it was parsed, and the connection is closed.
Nothing was sent to the key server.
429
RATE_LIMITED (retryable: true, Retry-After) -- the allowance this request is charged
to is spent: the login allowance of its network source (before authentication), or the
allowance, the concurrency ceiling or the live-session ceiling of the identity it was
verified as -- shared by every session that identity holds, including every delegated
and scoped credential a service minted. It carries no scope. Nothing was sent to the
key server; on a login refused after authentication, the session it created has been
closed. See "Rate and capacity limits" above.
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. INVALID_ARGUMENT is the one 400 that DID reach the key server: the key server itself refused the argument (ERR_INVALID_ARGUMENT), and detail is its own sentence. On POST /api/v1/certificates/enroll that is a certificate that cannot authenticate -- certificate not permitted for authentication: keyUsage is present without digitalSignature (...), ... extendedKeyUsage is present without clientAuth (...), or certificate expired at enrolment: notAfter <UTC> ... -- and a multi-certificate body that carries one such leaf persists nothing. KEY_UNWRAP_FAILED reached the key server too, on POST /api/v1/keys/import with format WRAPPED: the wrapped key did not unwrap under the key server's trust certificate -- wrapped to another key, by another method, or damaged. Nothing was stored for that key. |
401 | No live session, a DPoP proof that did not verify, or a certificate session whose certificate the key server now refuses. 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. CERTIFICATE_REVOKED -- on a session established by certificate login, the key server re-checks the certificate that session was established with on its operations, and it is now named by revocation evidence. detail is the key server's sentence. The Gateway answers this once and then ENDS everything that service holds or minted here -- its sessions, the delegated sessions and the scoped credentials it minted -- so a later operation on any of them is SESSION_NOT_FOUND. Log in with a different certificate. SSO_SESSION_EXPIRED -- a single-sign-on session (POST /api/v1/session/oidc) reached its lifetime, or was idle past its binding's idle bound; it has been ended. Sign in again. SENDER_KEY_RETIRED -- the session's registered sender key was rotated out and its overlap has passed (see "The registered service's credential lifecycle"); the session is closed with this answer. SERVICE_REGISTRATION_ENDED -- the registration a service credential was issued under was removed or narrowed on this Gateway; it has ended. Both are final for that credential. |
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. Or CAPACITY_UNAVAILABLE (scope: process, Retry-After) -- this Gateway is at its request ceiling (see "Rate and capacity limits"). |
413 | PAYLOAD_TOO_LARGE (retryable: false) -- the request body is larger than this deployment accepts. It was refused before it was parsed, and the connection is closed. Nothing was sent to the key server. |
429 | RATE_LIMITED (retryable: true, Retry-After) -- the allowance this request is charged to is spent: the login allowance of its network source (before authentication), or the allowance, the concurrency ceiling or the live-session ceiling of the identity it was verified as -- shared by every session that identity holds, including every delegated and scoped credential a service minted. It carries no scope. Nothing was sent to the key server; on a login refused after authentication, the session it created has been closed. See "Rate and capacity limits" above. |