This document is the source of truth for the wire contract between the
server and every client (the Rust secrets-client crate, the Go
secrets-manager-go module,
and any future client). Change this document and the server together; clients
track it.
- The server binds loopback only and speaks plain HTTP; TLS is terminated by
a reverse proxy in front of it. Clients therefore require an
https://server URL. Plainhttp://is accepted only for loopback hosts (127.0.0.0/8,::1,localhost) to support same-host use; any otherhttp://URL is refused. Base URLs must not contain userinfo, query, or fragment parts. - Every endpoint except
GET /v1/healthrequires a bearer token:Authorization: Bearer <token>. - A token may be scoped to a single project. A scoped token accessing any
other project receives
403.GET /v1/projectsreturns only the scoped project for such tokens. - An unscoped token is an admin token. Token management endpoints require an
admin token and return
403for scoped tokens. - Request and response bodies are JSON (
Content-Type: application/json). The server caps request bodies at 1 MiB (larger is413); clients cap response reads at 1 MiB. - Tokens can carry an expiry (90-day default at creation). Expired tokens
receive
401, exactly like revoked ones.
| Field | Rule |
|---|---|
| project name | non-empty, ≤ 64 bytes, [A-Za-z0-9_-] only |
| token name | non-empty, ≤ 64 bytes, [A-Za-z0-9_-] only |
| secret key | non-empty, ≤ 128 bytes, [A-Za-z0-9_.-] only |
Violations return 400.
| Code | Meaning |
|---|---|
| 200 | OK (body follows) |
| 201 | Created (project or token) |
| 204 | No Content |
| 400 | Bad Request (invalid name, malformed/invalid JSON body) |
| 401 | Unauthorized (missing/invalid/revoked/expired token) |
| 403 | Forbidden (token not scoped to this project) |
| 404 | Not Found (unknown project or key; the name itself was valid) |
| 409 | Conflict (project already exists) |
| 413 | Payload Too Large (request body over 1 MiB) |
Name validation runs before existence checks: a syntactically invalid
project or key name is always 400, never 404.
No error response ever includes secret material, token values, or the master passphrase.
200 OK
{ "status": "ok" }Scoped tokens see only their own project.
200 OK
{ "projects": [ { "name": "cdn", "created_at": "2026-07-04T12:00:00Z" } ] }// request
{ "name": "cdn" }
201 Created
{ "name": "cdn" }Returns 400 for an invalid name, 409 if the project already exists.
(Provisioning is an admin/API operation; the CLI clients do not expose it.)
Returns a flat map of decrypted key → value for the project.
200 OK
{ "DATABASE_URL": "postgres://…", "API_KEY": "…" }404 if the project does not exist.
The value is created or updated (upsert); version is the new monotonic
version of that key.
// request
{ "value": "postgres://…" }
200 OK
{ "key": "DATABASE_URL", "version": 2 }400 for an invalid key, 404 if the project does not exist.
204 No Content
404 if the project or key does not exist.
Requires an admin token. The response never includes plaintext tokens or token hashes.
200 OK
{
"tokens": [
{
"name": "ci",
"scope": "app",
"created_at": "2026-07-15T00:00:00Z",
"expires_at": "2026-10-13T00:00:00Z",
"revoked": false
}
]
}403 if the caller uses a scoped token.
Requires an admin token. The plaintext token is returned once in the response and is never stored by the server.
// request
{ "name": "windows", "project": "app", "ttl_days": 90 }
201 Created
{
"name": "windows",
"scope": "app",
"expires_at": "2026-10-13T00:00:00Z",
"token": "shown-once"
}Omit project for an admin token. Use "no_expiry": true instead of
ttl_days for a token without expiry. Returns 400 for invalid names or TTL,
and 403 if the caller uses a scoped token.
Requires an admin token. Revokes all active tokens with the given name.
200 OK
{ "revoked": 1 }Returns 400 for an invalid token name and 403 if the caller uses a scoped
token.
- Send the token only in the
Authorizationheader — never in the URL, query string, or logs. - Treat secret values as sensitive in memory: prefer a wrapping/zeroizable type
over plain strings, and never print them (including in
Debug/String). - Only transport-level failures should trigger any offline-cache fallback; auth/HTTP status failures (401/403/404) must not fall back to cache.