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.
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: 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 |
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 |
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: 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": "..."}
Response 200: {}. Clears must_change_password if it was set.
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.
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"}'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.
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: 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.