# 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](/sdks/go#lockwelladmin-lockwell-sdk-go-the-admin-client), [Node](/sdks/node#adminclient-the-admin-client), or
[Java](/sdks/java#lockwelladminclient-the-admin-client). 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:

```text
Authorization: Bearer lwadm_…
```

- Token bootstrap is offline-only (`lockwell admin-token create` needs filesystem access, the same trust model as
  `lockwell 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:bucket=reports:prefix=in/,op=write:bucket=reports:prefix=in/`) and an optional `expiresAt` (RFC3339 or
`YYYY-MM-DD`).

::: warning 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:

- `reason` is required on `disable`, `delete`, and `revoke` (a `400` otherwise).
- `confirm` must equal the tenant id on `delete` (a `400` otherwise).
- `?dryRun=true` on any mutation returns the plan the call would execute and applies nothing. For tenant
  `disable`/`delete` the 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.

```text
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](/guide/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:

```json
{
  "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`](https://github.com/RusticStack/lockwell/blob/main/internal/adminapi/openapi.json)
  (the canonical source; a human-readable YAML twin lives alongside at `internal/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](/reference/native-api#bucket-event-notifications).

## 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.

<OASpec />
