Skip to content

HTTP API reference

Every structured_log_server endpoint: parameters, request/response bodies, the specific errors each one can return, and a curl example. Shared object shapes live in models.md; the full error catalog (shared across endpoints) lives in errors.md — this document links to both rather than repeating them per endpoint.

This is a reading aid over the normative OpenSpec specs, not a replacement — it fills in the concrete wire-level shapes the specs leave abstract. If specs/*.md changes, it wins.

GET /v1/logs, /v1/audit-log, /v1/users, /v1/groups and /v1/projects are paginated the same way. Each takes optional limit and cursor and answers {"items": [...], "next_cursor": string | null}.

Param Notes
limit Default 50, ceiling 200 — a larger value is served at 200 rather than refused. Not a positive integer → 400 invalid_request.
cursor The previous response’s next_cursor, passed back untouched. A value the server did not issue → 400 invalid_request.

Items are ordered newest first (largest id first); cursor continues toward older ones, so a row created while you are paging never shifts a page. next_cursor is null on the last page — the server says so itself, so there is no need to request an empty page to find the end. There is no total: counting the audit log and the entries costs more than the pages are worth.

Lists that are bounded by the size of one group or one project — teams, team members, a project’s secret keys, role assignments — are not paginated and return everything in one response.

Breaking change: GET /v1/groups and GET /v1/projects used to return every visible row and now return the first page (50). A caller that read them whole must follow next_cursor.

Examples below use http://localhost:8080 as the server’s base URL, and shell variables $ACCESS_TOKEN (a JWT from POST /v1/auth/token) and $PROJECT_SECRET_KEY (from POST /v1/projects/:id/secret-keys, prefixed slk_) — substitute your own.

Cross-origin requests (CORS) (log-server-api)

Section titled “Cross-origin requests (CORS) (log-server-api)”

Off by default — every response below is written as it looks with no Origin header at all, or with one the operator hasn’t named (--cors-allowed-origins, unset by default; see configuration.md). This is what a same-origin deployment (deploy/) sees, and what every non-browser client sees regardless.

When the request carries an Origin header that exactly matches one of the configured origins (case-sensitive, no wildcard — a scheme/host/port triple, verbatim), two things change:

  • A preflight is answered before anything else. OPTIONS with an Access-Control-Request-Method header gets 204 immediately — ahead of rate limiting and authentication, on every endpoint, ingestion included — with:

    Access-Control-Allow-Origin: <the matched origin>
    Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
    Access-Control-Allow-Headers: Authorization, Content-Type
    Vary: Origin

    These three values are fixed constants (what this API actually uses), not an echo of what the browser asked for in Access-Control-Request-Method/-Headers.

  • Every other response, success or error, gets two headers added on top of whatever it already carries — Access-Control-Allow-Origin: <the matched origin> and Vary: Origin — so a browser can read a 401, 403, 429, or 5xx body too, not only a 200.

An Origin that doesn’t match any configured value is treated exactly like no Origin at all — no CORS headers, and no separate rejection response. See README.md for where this sits relative to rate limiting and authentication.

Log ingestion, query, and live stream (log-server-api, log-server-live-stream)

Section titled “Log ingestion, query, and live stream (log-server-api, log-server-live-stream)”

Spec: specs/log-server-api/spec.md, specs/log-server-live-stream/spec.md.

Auth: Authorization: Bearer <project-secret-key> — the key’s value carries the slk_ prefix that tells it apart from an access token in the header both schemes share. Presenting an access token here answers 401, exactly as a missing credential does.

Request body: JSON array of free-form log-entry objects — see models.md#logentry. No wrapper object; the array is the entire body.

Both natural guesses are refused, with 400 invalid_request and "Request body must be a JSON array.": a wrapper such as {"entries": [...]}, which most batch APIs would take, and a single entry object sent on its own without the enclosing array. Wrap one entry as a one-element array. An empty array and an empty body are both valid, and accepted as zero entries.

Response 202: Ingestion response — always 202 if the request itself is well-formed/authenticated/ under the size limit and the project isn’t blocked, even if every entry was rejected; see errors.md.

Errors: 400 invalid_request (body is not a JSON array, or is not valid JSON at all), 401 unauthorized (bad/unknown/revoked key), 403 project_blocked, 413 payload_too_large. Per-entry validation_error/quota_exceeded are reported inside the 202 body, not as HTTP errors.

Terminal window
curl -X POST http://localhost:8080/v1/logs \
-H "Authorization: Bearer $PROJECT_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '[
{"event": "payment_failed", "level": "error", "timestamp": "2026-03-05T14:29:59.981Z",
"category": "checkout", "order_id": "ord_44821"},
{"event": "request_completed", "level": "info", "timestamp": "2026-03-05T14:30:00.100Z"}
]'

Auth: Authorization: Bearer <access-token>.

Query parameters:

Param Type Notes
project_id xor group_id integer Exactly one required
level string Minimum level (inclusive)
category, logger string Exact match
from, to ISO 8601 Range on timestamp. A value that cannot be read, or one whose components do not exist (2026-13-01, 2026-02-30), is 400 invalid_request — not a dropped filter
session_id, request_id, connection_generation, tool_call_id, message_id, operation_id string Exact match
q string Full-text, matched against event and content
context.<key> string Exact match on a custom field, e.g. context.order_id=ord_44821. A dotted key reaches into a nested object (context.order.id). The key may not have an empty segment — context., context..x, context.. and a trailing dot (context.a.) are 400 invalid_request
limit, cursor Pagination

Response 200: {"items": [LogEntry], "next_cursor": string \| null} — see models.md#logentry. Without cursor, items is ordered newest-first by id; cursor advances toward older entries.

Errors: 400 invalid_request (an unreadable from/to, limit/cursor, or a context.<key> with an empty segment), 403 forbidden (no grant covering the scope), 403 project_blocked (direct project_id only — a group_id query silently drops the blocked project’s entries instead), 404 not_found (project_id/group_id doesn’t exist).

Terminal window
curl -G http://localhost:8080/v1/logs \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d project_id=7 \
-d level=warning \
-d from=2026-03-05T00:00:00Z \
-d limit=50

Auth: Authorization: Bearer <access-token>.

Query parameters: same as GET /v1/logs above, plus since_id (integer, optional — catch-up cutoff, see live-streaming.md). No limit/cursor — this is a stream, not a page.

Response 200: Content-Type: text/event-stream; frames are id: <log entry id> / event: log / data: <LogEntry as JSON>, plus periodic : ping keep-alive comments and a possible terminal event: end — full framing in live-streaming.md.

Errors: same as GET /v1/logs, returned as a normal (non-streamed) response before the connection upgrades — a rejected subscription never opens a stream that then errors.

Terminal window
curl -N http://localhost:8080/v1/logs/stream \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-G -d project_id=7 -d level=warning

Auth: none.

Response 200: {"status": "ok"}.

Errors: none defined — the endpoint doesn’t respond (connection refused/timeout) rather than returning an error status while the server isn’t ready.

Terminal window
curl http://localhost:8080/healthz

Authentication and password recovery (log-server-auth, log-server-password-reset)

Section titled “Authentication and password recovery (log-server-auth, log-server-password-reset)”

Spec: specs/log-server-auth/spec.md, specs/log-server-password-reset/spec.md, specs/log-server-email-verification/spec.md. See auth.md.

Planned, not implemented: POST /v1/auth/register, POST /v1/auth/verify-email, POST /v1/auth/verify-email/resend, POST /v1/auth/password-reset, and POST /v1/auth/password-reset/confirm — five of the nine endpoints below — are part of the originally specified design (linked above) but have no route in the running server; each is marked individually below. Every account is created by an administrator or a group owner instead (POST /v1/users), and a forgotten password is reset the same way (PATCH /v1/users/:id) — see the Developer Guide.

Every endpoint in this section is rate-limited (log-server-rate-limit), along with POST /v1/auth/change-password and DELETE /v1/users/me: a rejected request answers 429 too_many_requests with a Retry-After header and the general JSON envelope — including the token endpoint, the one place it departs from the RFC 6749 shape (errors.md). A 429 means the action never ran: no password was checked, no token issued, no email sent, and no account was locked (auth.md). The error lists below don’t repeat 429 per endpoint.

Planned, not implemented — see the note at the top of this section.

Auth: none. JSON body, not form-encoded — unlike the token endpoint below, this path isn’t part of the RFC 6749 token contract, so it follows this API’s normal JSON convention.

Request body:

Field Type Required
username string yes
password string yes
email string yes — mandatory on this path specifically
display_name string no

Response 201: User (no RoleAssignment yet, email_verified_at: null). A verification email is sent as a side effect — the account cannot log in via grant_type=password until it’s confirmed, see below and auth.md.

Errors: 400 invalid_request (missing email/username/password), 403 forbidden (registrationEnabled = false), 409 username_taken, 409 email_taken.

Terminal window
curl -X POST http://localhost:8080/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "correct-horse-battery-staple", "email": "alice@example.com"}'

Planned, not implemented — see the note at the top of this section.

Auth: none (the verification token is the credential). JSON body.

Request body: {"token": "..."}

Response 200: {}. Sets email_verified_at, after which grant_type=password works normally for this account.

Errors: 400 invalid_token (unknown/expired/already-used token).

Terminal window
curl -X POST http://localhost:8080/v1/auth/verify-email \
-H "Content-Type: application/json" \
-d '{"token": "a1b2c3..."}'

Planned, not implemented — see the note at the top of this section.

Auth: none. JSON body.

Request body: {"email": "..."}

Response 202: {} — always, regardless of whether the email is registered or already verified (anti-enumeration, same pattern as password-reset).

Errors: 400 invalid_request (missing email).

Terminal window
curl -X POST http://localhost:8080/v1/auth/verify-email/resend \
-H "Content-Type: application/json" \
-d '{"email": "alice@example.com"}'

Auth: none. Form-encoded (application/x-www-form-urlencoded), RFC 6749 — the one endpoint in this API that deviates from the general JSON envelope on both request and error response, for compatibility with off-the-shelf OAuth2 clients (auth.md).

Request body (grant_type=password): grant_type=password&username=...&password=...

Request body (grant_type=refresh_token): grant_type=refresh_token&refresh_token=...

Response 200: Token response.

Errors: 429 too_many_requests (general envelope, see above); otherwise all 400, RFC shape — invalid_request (missing field for the given grant_type), unsupported_grant_type, invalid_grant (wrong credentials; unknown/expired/revoked refresh token; blocked user on refresh; unverified email on grant_type=password — response additionally carries reason: "email_not_verified", see errors.md).

Terminal window
curl -X POST http://localhost:8080/v1/auth/token \
-d grant_type=password \
-d username=alice \
-d password=correct-horse-battery-staple
Terminal window
curl -X POST http://localhost:8080/v1/auth/token \
-d grant_type=refresh_token \
-d refresh_token=$REFRESH_TOKEN

Auth: none (the refresh token being revoked is the credential). Form-encoded, RFC 7009.

Request body: refresh_token=...

Response 200: {} — always, whether or not the token was valid (anti-enumeration, auth.md).

Errors: 400 invalid_request (RFC shape) only if the refresh_token field itself is missing from the body.

Terminal window
curl -X DELETE http://localhost:8080/v1/auth/token \
-d refresh_token=$REFRESH_TOKEN

Planned, not implemented — see the note at the top of this section.

Auth: none. JSON body.

Request body: {"email": "..."}

Response 202: {} — always, regardless of whether the email is registered (anti-enumeration).

Errors: 400 invalid_request (missing email).

Terminal window
curl -X POST http://localhost:8080/v1/auth/password-reset \
-H "Content-Type: application/json" \
-d '{"email": "alice@example.com"}'

Planned, not implemented — see the note at the top of this section.

Auth: none (the reset token is the credential). JSON body.

Request body: {"token": "...", "new_password": "..."}

Response 200: {}.

Errors: 400 invalid_token (unknown/expired/already-used token), 400 invalid_request (missing field).

Terminal window
curl -X POST http://localhost:8080/v1/auth/password-reset/confirm \
-H "Content-Type: application/json" \
-d '{"token": "a1b2c3...", "new_password": "even-better-passphrase"}'

Auth: Authorization: Bearer <access-token>. JSON body.

Request body: {"password": "..."}

Response 204: empty body.

Errors: 401 invalid_grant (wrong password), 403 cannot_delete_primary_admin, 409 sole_group_owner (details.blocking_groups).

Terminal window
curl -X DELETE http://localhost:8080/v1/users/me \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"password": "correct-horse-battery-staple"}'

Auth: Authorization: Bearer <access-token>. JSON body. Available to any authenticated role, over their own account only — not just while must_change_password is set (log-server-forced-password-change, auth.md).

Request body: {"current_password": "...", "new_password": "...", "keep_other_sessions": false, "current_refresh_token": "..."} — the last two optional.

Response 200: {}. Clears must_change_password if it was set.

A successful change also ends the account’s other sessions: every refresh token it holds is revoked except the one named in current_refresh_token. Bumping token_version alone would not do this — that retires access tokens, and a refresh token outlives it and mints a new one, so a password changed because somebody else learned it would not have removed them.

The safe behaviour is the default, so a client that sends neither field gets the sweep. Which means a caller that cannot name its own token — including one that simply doesn’t send it — is signed out along with everyone else: the server has no other way to recognise the session asking, since this endpoint authenticates with an access token and stored refresh tokens are hashes that say nothing about whose device they are. Send current_refresh_token to stay signed in; keep_other_sessions: true to leave every session alone.

Errors: 401 invalid_grant (wrong current password); 400 invalid_request if new_password is shorter than 8 characters (details.reason: "too_short", min_length) or longer than 72 bytes in UTF-8 ("too_long", max_bytes) — the same rule applies wherever a password is set (POST /v1/users, PATCH /v1/users/:id). It is checked when a password is chosen, never at login. 400 invalid_request with details.field naming the field if keep_other_sessions is not a boolean or current_refresh_token is not a string.

Terminal window
curl -X POST http://localhost:8080/v1/auth/change-password \
-H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"current_password": "temp-password-123", "new_password": "a-much-better-passphrase",
"current_refresh_token": "'"$REFRESH_TOKEN"'"}'

Users, groups, teams, roles (log-server-rbac)

Section titled “Users, groups, teams, roles (log-server-rbac)”

Spec: specs/log-server-rbac/spec.md, specs/log-server-forced-password-change/spec.md. See rbac-and-lifecycle.md. All endpoints below: Authorization: Bearer <access-token>, JSON bodies.

Role: admin.

Request body: same fields as POST /v1/auth/register, but email is optional here.

Response 201: User — must_change_password: true always (log-server-forced-password-change); email_verified_at: null if email was set.

Errors: 400 invalid_request (missing username/password, or a password outside 8 characters – 72 bytes, details.reason too_short/too_long), 403 forbidden, 409 username_taken, 409 email_taken.

Terminal window
curl -X POST http://localhost:8080/v1/users \
-H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"username": "bob", "password": "temp-password-123", "display_name": "Bob Diaz"}'

Role: admin.

Query: username (substring match), limit, cursor — see Pagination.

Response 200: {"items": [User], "next_cursor": string \| null}.

Errors: 403 forbidden.

Terminal window
curl -G http://localhost:8080/v1/users -H "Authorization: Bearer $ACCESS_TOKEN" -d limit=50

Role: admin. Partial update — any subset of the fields below.

Request body:

Field Type Notes
email string A new value resets email_verified_at to null and triggers a fresh verification email
display_name string
password string Setting this always sets must_change_password: true and revokes all of the target’s refresh tokens

Response 200: User (updated).

Errors: 403 forbidden, 404 not_found, 409 email_taken.

Terminal window
curl -X PATCH http://localhost:8080/v1/users/42 \
-H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"password": "new-temp-password-456"}'

POST /v1/users/:id/block / POST /v1/users/:id/unblock

Section titled “POST /v1/users/:id/block / POST /v1/users/:id/unblock”

Role: admin. No request body.

Response 200: User (updated is_active).

Errors: 403 forbidden, 404 not_found; unblock additionally: 409 deleted_account.

Terminal window
curl -X POST http://localhost:8080/v1/users/42/block -H "Authorization: Bearer $ACCESS_TOKEN"
curl -X POST http://localhost:8080/v1/users/42/unblock -H "Authorization: Bearer $ACCESS_TOKEN"

Role: admin. No request body — target’s password is never required.

Response 204: empty body.

Errors: 400 self_deletion_requires_me (:id == caller), 403 forbidden, 403 cannot_delete_primary_admin, 404 not_found, 409 sole_group_owner.

Terminal window
curl -X DELETE http://localhost:8080/v1/users/99 -H "Authorization: Bearer $ACCESS_TOKEN"

Role: admin.

Request body: {"name": "..."}

Response 201: Group.

Errors: 403 forbidden.

Terminal window
curl -X POST http://localhost:8080/v1/groups \
-H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"name": "payments-team"}'

Role: any authenticated user; results scoped to visible groups. What the caller may see is part of the query, so a caller with one group among a thousand gets it on the first page.

Query: name (substring, case-insensitive), limit, cursor — see Pagination.

Response 200: {"items": [Group], "next_cursor": string \| null}.

Terminal window
curl http://localhost:8080/v1/groups -H "Authorization: Bearer $ACCESS_TOKEN"

Role: owner of :groupId, or admin.

Request body: {"name": "..."}

Response 201: Team.

Errors: 403 forbidden, 404 not_found (:groupId).

Terminal window
curl -X POST http://localhost:8080/v1/groups/3/teams \
-H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"name": "on-call"}'

Role: any role with read access to :groupId (admin/owner/user).

Response 200: {"items": [Team]}.

Errors: 403 forbidden, 404 not_found (:groupId).

Terminal window
curl http://localhost:8080/v1/groups/3/teams -H "Authorization: Bearer $ACCESS_TOKEN"

Role: same as above. Minimal shape, not a full User — just enough to show and pick a member: {"items": [{"user_id": 42, "username": "alice"}]}.

Errors: 403 forbidden, 404 not_found (:teamId).

Terminal window
curl http://localhost:8080/v1/teams/5/members -H "Authorization: Bearer $ACCESS_TOKEN"

Role: owner of the team’s group, or admin. Bumps token_version for the added user only, not the team’s other members — their own access is unaffected by someone else joining (auth.md).

Request body: {"user_id": 42}

Response 204: empty body.

Errors: 403 forbidden, 404 not_found (:teamId or user_id).

Terminal window
curl -X POST http://localhost:8080/v1/teams/5/members \
-H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"user_id": 42}'

Role: owner of the team’s group, or admin. Bumps token_version for the removed user only — the same single-user effect as above, applied to whoever just lost access.

Response 204: empty body.

Errors: 403 forbidden, 404 not_found, 409 sole_group_owner (the member is the last one of a team that owns a group and nobody else owns it — details.blocking_groups).

Terminal window
curl -X DELETE http://localhost:8080/v1/teams/5/members/42 -H "Authorization: Bearer $ACCESS_TOKEN"

Role: admin (any grant), or owner (owner/user within their own group/its projects only).

Request body:

Field Type Notes
subject_type "user" | "team"
subject_id integer
role "admin" | "owner" | "user"
scope_type "global" | "group" | "project"
scope_id integer Required unless scope_type: "global"

Response 201: RoleAssignment.

Errors: 400 invalid_request, 403 forbidden (including an owner attempting role: admin, or a scope outside their own group), 404 not_found (subject_id/scope_id).

Terminal window
curl -X POST http://localhost:8080/v1/role-assignments \
-H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"subject_type": "user", "subject_id": 42, "role": "user", "scope_type": "project", "scope_id": 7}'

Role: same rule as creating it.

Response 204: empty body.

Errors: 403 forbidden, 404 not_found, 409 sole_group_owner (revoking the last owner grant on a group — details.blocking_groups).

Terminal window
curl -X DELETE http://localhost:8080/v1/role-assignments/128 -H "Authorization: Bearer $ACCESS_TOKEN"

Projects and quotas (log-server-rbac, log-server-quotas)

Section titled “Projects and quotas (log-server-rbac, log-server-quotas)”

Spec: specs/log-server-quotas/spec.md. See quotas-and-audit.md. All endpoints: Authorization: Bearer <access-token>, JSON bodies.

Role: any authenticated user; every project the caller may read, flat — a role on one project does not cover its group, so this is such a user’s only way to find it.

Query: group_id, name (substring, case-insensitive), limit, cursor — see Pagination.

Response 200: {"items": [Project], "next_cursor": string \| null}. Carries is_blocked, not the usage counters — those come from GET /v1/projects/:id.

Terminal window
curl -G http://localhost:8080/v1/projects -H "Authorization: Bearer $ACCESS_TOKEN" -d limit=50

Role: owner of :groupId, or admin.

Request body:

Field Type Required
name string yes
retention_days integer yes
max_entries integer no
max_bytes integer no

Response 201: Project (without entry_count/total_bytes).

Errors: 400 invalid_request (missing retention_days), 403 forbidden, 404 not_found (:groupId).

Terminal window
curl -X POST http://localhost:8080/v1/groups/3/projects \
-H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"name": "checkout-service", "retention_days": 30, "max_entries": 1000000}'

Role: owner/admin.

Request body: any of retention_days/max_entries/max_bytes (partial update).

Response 200: Project (updated, without entry_count/total_bytes).

Errors: 403 forbidden, 404 not_found.

Terminal window
curl -X PATCH http://localhost:8080/v1/projects/7 \
-H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"max_entries": 2000000}'

Role: owner/user with access, or admin.

Response 200: Project, with entry_count/total_bytes.

Errors: 403 forbidden, 404 not_found.

Terminal window
curl http://localhost:8080/v1/projects/7 -H "Authorization: Bearer $ACCESS_TOKEN"

POST /v1/projects/:id/block / POST /v1/projects/:id/unblock

Section titled “POST /v1/projects/:id/block / POST /v1/projects/:id/unblock”

Role: admin only — not owner, even for their own project. No request body.

Response 200: Project (updated is_blocked).

Errors: 403 forbidden, 404 not_found.

Terminal window
curl -X POST http://localhost:8080/v1/projects/7/block -H "Authorization: Bearer $ACCESS_TOKEN"
curl -X POST http://localhost:8080/v1/projects/7/unblock -H "Authorization: Bearer $ACCESS_TOKEN"

Role: owner/admin.

Request body: {"label": "..."} (optional).

Response 201: ProjectSecretKey, with secret — shown exactly this once.

Errors: 403 forbidden, 404 not_found.

Terminal window
curl -X POST http://localhost:8080/v1/projects/7/secret-keys \
-H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"label": "prod-checkout-instance-1"}'

Role: owner/user with access, or admin.

Response 200: {"items": [ProjectSecretKey]} — metadata only, never secret.

Errors: 403 forbidden, 404 not_found.

Terminal window
curl http://localhost:8080/v1/projects/7/secret-keys -H "Authorization: Bearer $ACCESS_TOKEN"

DELETE /v1/projects/:id/secret-keys/:keyId

Section titled “DELETE /v1/projects/:id/secret-keys/:keyId”

Role: owner/admin. Irreversible.

Response 204: empty body.

Errors: 403 forbidden, 404 not_found.

Terminal window
curl -X DELETE http://localhost:8080/v1/projects/7/secret-keys/15 -H "Authorization: Bearer $ACCESS_TOKEN"

Spec: specs/log-server-audit/spec.md. See quotas-and-audit.md.

Auth: Authorization: Bearer <access-token>. Role: admin only — not owner.

Query parameters: actor_user_id, action, target_type, target_id, from, to, limit, cursor. An unknown action and an unreadable from/to are both 400 invalid_request rather than an empty page: in a journal answering “did this happen”, “no records” and “you misspelled it” must not look alike.

Response 200: {"items": [AuditLogEntry], "next_cursor": string \| null, "audit_retention_days": integer \| null, "auth_event_retention_days": integer \| null} — the two retention fields report the policy in force (null = kept indefinitely), so an empty result outside the window explains itself (quotas-and-audit.md).

Errors: 400 invalid_request (unknown action, unreadable from/to, limit/cursor), 403 forbidden.

There is no endpoint that deletes audit entries, for any role, and the retention periods are set in the server’s configuration rather than through the API — an admin is a subject of this log, not its owner. Entries disappear only through the operator’s configured policy, and each purge that removed anything leaves an audit.purged entry behind.

Terminal window
curl -G http://localhost:8080/v1/audit-log \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d action=user.blocked \
-d limit=50
  • models.md — full object shapes.
  • errors.md — the complete error catalog, including the distinction between per-entry batch codes and top-level HTTP errors, and why 403 sometimes stands in for 404.
  • configuration.md — --cors-allowed-origins and every other setting.