Admin API
The JSON Admin API lives at /admin/api/v1/ on the admin listener (never the public S3 port). It is the control plane: tenants, service accounts, scoped access keys, quotas, usage, and audit.
It reuses the exact in-process domain services the HTML admin UI uses (tenant lifecycle, the metadata repo, the SigV4 secret cipher, the auditor), so it cannot bypass any authorization, audit, retention, or quota gate.
For most apps, reach this API through a first-party SDK rather than calling it directly: Go, Node, or Java. This page is the wire-level reference.
Authentication
Authentication is by an admin API bearer token: a high-entropy secret minted offline with lockwell admin-token create, stored only hashed (SHA-256), and distinct from S3 access keys. The wire form is prefixed lwadm_…. Send it as:
Authorization: Bearer lwadm_…- Token bootstrap is offline-only (
lockwell admin-token createneeds filesystem access, the same trust model aslockwell admin-create). There is no JSON route to mint the first token. - Bearer tokens are not sent automatically by browsers, so the API is for server-to-server use and carries no CSRF flow.
- Anonymous, unauthenticated, revoked, and expired tokens are denied with
401. - Every request, success and denial, writes an audit row through the existing auditor. Tokens are rate-limited per token.
RBAC roles
Authorization composes the token's RBAC role with an optional single-tenant scope. A tenant-scoped token cannot cross to another tenant: a cross-tenant target is a 403, never a 404 existence leak.
| Role | Can do |
|---|---|
viewer | read-only: list/get tenants, accounts, keys, quota, usage, audit |
operator | the above plus create tenants/accounts/keys, set/clear quota, rotate/revoke keys |
owner | the above plus the destructive tenant lifecycle: disable and delete |
Routes
The server base is /admin/api/v1. GET /healthz and GET /openapi.json are unauthenticated; everything else requires a valid admin token.
Tenants
| Method + path | Role | Purpose |
|---|---|---|
GET /tenants | viewer | list tenants (global token = all; scoped = its tenant) |
POST /tenants | operator | create a tenant |
GET /tenants/{id} | viewer | get one tenant |
POST /tenants/{id}/disable | owner | disable a tenant; reason required |
POST /tenants/{id}/delete | owner | delete a disabled tenant; reason + confirm required |
disable revokes the tenant's active access keys. delete requires the tenant to be disabled first, requires confirm to equal the tenant id, and fails closed with a 412 when a retention window or legal hold gates the delete.
Accounts, keys, quota, usage
| Method + path | Role | Purpose |
|---|---|---|
GET /tenants/{id}/accounts | viewer | list service accounts |
POST /tenants/{id}/accounts | operator | create a service account |
GET /tenants/{id}/keys | viewer | list access keys (secrets never returned) |
POST /tenants/{id}/keys | operator | create an access key (secret returned once) |
POST /tenants/{id}/keys/{keyId}/rotate | operator | rotate a key (new secret returned once) |
POST /tenants/{id}/keys/{keyId}/revoke | operator | revoke a key; reason required |
GET|PUT|DELETE /tenants/{id}/quota | viewer / operator | get / set / clear the tenant quota |
GET /tenants/{id}/usage | viewer | storage usage report |
The secret on a created or rotated key is shown exactly once and is never recoverable afterward; persist it immediately. listKeys returns only metadata.
The key request body accepts a scopes string (verb list read,write,delete,admin, or the resource form op=read,write,delete:bucket=reports:prefix=in/) and an optional expiresAt (RFC3339 or YYYY-MM-DD).
A created or rotated key returns its secret exactly once. Capture it from the response immediately; there is
no endpoint that reads it back. :::
Audit
| Method + path | Role | Purpose |
|---|---|---|
GET /audit?tenant=&since=&limit= | viewer | query the audit log |
since is a Go duration string (e.g. 24h); limit is clamped to [1, 1000] (default 100). A tenant-scoped token is forced to its own tenant regardless of the tenant parameter; an explicit cross-tenant tenant from a scoped token is a 403.
reason / confirm and dry runs
Destructive operations are gated on the wire:
reasonis required ondisable,delete, andrevoke(a400otherwise).confirmmust equal the tenant id ondelete(a400otherwise).?dryRun=trueon any mutation returns the plan the call would execute and applies nothing. For tenantdisable/deletethe plan enumerates the buckets, objects, versions, delete markers, legal-held and retained versions, access keys, and physical bytes that would be affected, so you can preview an offboarding before committing it.
POST /admin/api/v1/tenants/acme/delete?dryRun=true
Authorization: Bearer lwadm_…
Content-Type: application/json
{ "reason": "offboarding", "confirm": "acme" }The SDKs surface this directly: every mutation takes a dryRun option (Go/Node) or has a …DryRun twin (Java). See tenancy and auth.
Error shape
Failures return an RFC-7807-style JSON problem with a stable code, message, status, and requestId (also echoed in X-Request-Id), so an operator can correlate a failure with its audit row:
{
"code": "precondition_failed",
"message": "tenant has legal-held object versions",
"status": 412,
"requestId": "req_01HXY…"
}Status mapping:
| Status | Meaning |
|---|---|
400 | validation (missing reason, bad confirm, malformed body) |
401 | missing/invalid/revoked/expired admin token |
403 | RBAC role or cross-tenant scope denial |
404 | target not found |
412 | retention / legal-hold gated tenant delete (fail-closed) |
429 | per-token rate limit exceeded |
OpenAPI
The full machine-readable contract is an OpenAPI 3 document, available two ways:
- Served live at
GET /admin/api/v1/openapi.json(unauthenticated, since an operator needs the contract before holding a token; it leaks no tenant data). - Committed at
internal/adminapi/openapi.json(the canonical source; a human-readable YAML twin lives alongside atinternal/adminapi/openapi.yaml).
Each operation carries a unique operationId (listTenants, createTenantKey, queryAuditLog, …), so you can generate a client in any language with openapi-generator. Prefer the first-party Go/Node/Java admin clients; codegen is the path for every other language.
Not exposed here
This JSON API mirrors the tenant / account / key / quota / audit subset of the admin surface. Operational controls that live on the HTML admin UI (encryption-key rotation and rewrap, lifecycle, repair/scrub, placement, backup/restore) are not part of the JSON Admin API.
There is also no public-access, bucket-policy, or notification configuration surface here. Notifications are configured on the native data plane.
Interactive reference
Every admin operation below is generated from the OpenAPI document, so it always matches the shipped server. Expand an operation for its parameters, request and response schemas, and copy-paste samples. Send the admin token as Authorization: Bearer <token>. The example host is a placeholder; swap in your own admin endpoint.
Versioned, authenticated, RBAC'd, audited JSON Admin API for Lockwell. Mounted under /admin/api/v1/ on the admin/web listener only (never the public S3 data-plane port). Authentication is by admin API bearer token; tokens are minted offline with lockwell admin-token create, stored only hashed, scoped by RBAC role (owner/operator/viewer) and optional single-tenant scope. Every request, success and denial, writes an audit row. Server-to-server use only; bearer tokens are not sent automatically by browsers, so there is no CSRF flow.
Servers
tenants
Tenant lifecycle, quota, accounts, and keys.