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.
Pagination (log-server-pagination)
Section titled “Pagination (log-server-pagination)”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/groupsandGET /v1/projectsused to return every visible row and now return the first page (50). A caller that read them whole must follownext_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.
OPTIONSwith anAccess-Control-Request-Methodheader gets204immediately — 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, OPTIONSAccess-Control-Allow-Headers: Authorization, Content-TypeVary: OriginThese 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>andVary: Origin— so a browser can read a401,403,429, or5xxbody too, not only a200.
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.
POST /v1/logs
Section titled “POST /v1/logs”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.
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"} ]'GET /v1/logs
Section titled “GET /v1/logs”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).
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=50GET /v1/logs/stream
Section titled “GET /v1/logs/stream”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.
curl -N http://localhost:8080/v1/logs/stream \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -G -d project_id=7 -d level=warningGET /healthz
Section titled “GET /healthz”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.
curl http://localhost:8080/healthzAuthentication 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, andPOST /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.
POST /v1/auth/register
Section titled “POST /v1/auth/register”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.
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"}'POST /v1/auth/verify-email
Section titled “POST /v1/auth/verify-email”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).
curl -X POST http://localhost:8080/v1/auth/verify-email \ -H "Content-Type: application/json" \ -d '{"token": "a1b2c3..."}'POST /v1/auth/verify-email/resend
Section titled “POST /v1/auth/verify-email/resend”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).
curl -X POST http://localhost:8080/v1/auth/verify-email/resend \ -H "Content-Type: application/json" \ -d '{"email": "alice@example.com"}'POST /v1/auth/token
Section titled “POST /v1/auth/token”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).
curl -X POST http://localhost:8080/v1/auth/token \ -d grant_type=password \ -d username=alice \ -d password=correct-horse-battery-staplecurl -X POST http://localhost:8080/v1/auth/token \ -d grant_type=refresh_token \ -d refresh_token=$REFRESH_TOKENDELETE /v1/auth/token
Section titled “DELETE /v1/auth/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.
curl -X DELETE http://localhost:8080/v1/auth/token \ -d refresh_token=$REFRESH_TOKENPOST /v1/auth/password-reset
Section titled “POST /v1/auth/password-reset”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).
curl -X POST http://localhost:8080/v1/auth/password-reset \ -H "Content-Type: application/json" \ -d '{"email": "alice@example.com"}'POST /v1/auth/password-reset/confirm
Section titled “POST /v1/auth/password-reset/confirm”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).
curl -X POST http://localhost:8080/v1/auth/password-reset/confirm \ -H "Content-Type: application/json" \ -d '{"token": "a1b2c3...", "new_password": "even-better-passphrase"}'DELETE /v1/users/me
Section titled “DELETE /v1/users/me”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).
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"}'POST /v1/auth/change-password
Section titled “POST /v1/auth/change-password”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.
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.
POST /v1/users
Section titled “POST /v1/users”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.
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"}'GET /v1/users
Section titled “GET /v1/users”Role: admin.
Query: username (substring match), limit, cursor — see Pagination.
Response 200: {"items": [User], "next_cursor": string \| null}.
Errors: 403 forbidden.
curl -G http://localhost:8080/v1/users -H "Authorization: Bearer $ACCESS_TOKEN" -d limit=50PATCH /v1/users/:id
Section titled “PATCH /v1/users/:id”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.
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.
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"DELETE /v1/users/:id
Section titled “DELETE /v1/users/:id”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.
curl -X DELETE http://localhost:8080/v1/users/99 -H "Authorization: Bearer $ACCESS_TOKEN"POST /v1/groups
Section titled “POST /v1/groups”Role: admin.
Request body: {"name": "..."}
Response 201: Group.
Errors: 403 forbidden.
curl -X POST http://localhost:8080/v1/groups \ -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \ -d '{"name": "payments-team"}'GET /v1/groups
Section titled “GET /v1/groups”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}.
curl http://localhost:8080/v1/groups -H "Authorization: Bearer $ACCESS_TOKEN"POST /v1/groups/:groupId/teams
Section titled “POST /v1/groups/:groupId/teams”Role: owner of :groupId, or admin.
Request body: {"name": "..."}
Response 201: Team.
Errors: 403 forbidden, 404 not_found (:groupId).
curl -X POST http://localhost:8080/v1/groups/3/teams \ -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \ -d '{"name": "on-call"}'GET /v1/groups/:groupId/teams
Section titled “GET /v1/groups/:groupId/teams”Role: any role with read access to :groupId (admin/owner/user).
Response 200: {"items": [Team]}.
Errors: 403 forbidden, 404 not_found (:groupId).
curl http://localhost:8080/v1/groups/3/teams -H "Authorization: Bearer $ACCESS_TOKEN"GET /v1/teams/:teamId/members
Section titled “GET /v1/teams/:teamId/members”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).
curl http://localhost:8080/v1/teams/5/members -H "Authorization: Bearer $ACCESS_TOKEN"POST /v1/teams/:teamId/members
Section titled “POST /v1/teams/:teamId/members”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).
curl -X POST http://localhost:8080/v1/teams/5/members \ -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \ -d '{"user_id": 42}'DELETE /v1/teams/:teamId/members/:userId
Section titled “DELETE /v1/teams/:teamId/members/:userId”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).
curl -X DELETE http://localhost:8080/v1/teams/5/members/42 -H "Authorization: Bearer $ACCESS_TOKEN"POST /v1/role-assignments
Section titled “POST /v1/role-assignments”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).
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}'DELETE /v1/role-assignments/:id
Section titled “DELETE /v1/role-assignments/:id”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).
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.
GET /v1/projects
Section titled “GET /v1/projects”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.
curl -G http://localhost:8080/v1/projects -H "Authorization: Bearer $ACCESS_TOKEN" -d limit=50POST /v1/groups/:groupId/projects
Section titled “POST /v1/groups/:groupId/projects”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).
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}'PATCH /v1/projects/:id
Section titled “PATCH /v1/projects/:id”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.
curl -X PATCH http://localhost:8080/v1/projects/7 \ -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \ -d '{"max_entries": 2000000}'GET /v1/projects/:id
Section titled “GET /v1/projects/:id”Role: owner/user with access, or admin.
Response 200: Project, with entry_count/total_bytes.
Errors: 403 forbidden, 404 not_found.
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.
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"POST /v1/projects/:id/secret-keys
Section titled “POST /v1/projects/:id/secret-keys”Role: owner/admin.
Request body: {"label": "..."} (optional).
Response 201: ProjectSecretKey, with secret — shown exactly this once.
Errors: 403 forbidden, 404 not_found.
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"}'GET /v1/projects/:id/secret-keys
Section titled “GET /v1/projects/:id/secret-keys”Role: owner/user with access, or admin.
Response 200: {"items": [ProjectSecretKey]} — metadata only, never secret.
Errors: 403 forbidden, 404 not_found.
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.
curl -X DELETE http://localhost:8080/v1/projects/7/secret-keys/15 -H "Authorization: Bearer $ACCESS_TOKEN"Audit log (log-server-audit)
Section titled “Audit log (log-server-audit)”Spec: specs/log-server-audit/spec.md. See quotas-and-audit.md.
GET /v1/audit-log
Section titled “GET /v1/audit-log”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.
curl -G http://localhost:8080/v1/audit-log \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -d action=user.blocked \ -d limit=50See also
Section titled “See also”- 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
403sometimes stands in for404. - configuration.md —
--cors-allowed-originsand every other setting.