Skip to content

HTTP API Reference

All product API endpoints are mounted under:

/api/v1

Operational endpoints (/health, /ready, /metrics) are at the root (relative to your Keydock base URL — for example http://127.0.0.1:8080).

On a running Keydock instance:

  • Swagger UI — GET /swagger-ui/ (interactive explorer)
  • OpenAPI JSON — GET /api-docs/openapi.json

These paths are served by Keydock itself, not by this documentation site — open them against your deployment origin.


Credentials can be sent in three ways, evaluated in this order:

  1. Authorization: Bearer <credential>
  2. Authorization: Basic <base64(username:password)> — username is used as the credential
  3. Query parameters: ?access_token=<credential> or ?key=<credential>

Bucket credentials:

CredentialAccess
secret_keyAdmin — all operations including policy management and token minting
read_keyRead and list keys
write_keyWrite keys
Temporary tokenScoped by prefix and explicit permissions

Anonymous access depends on which credentials are configured. A bucket with no credentials is fully public. Configuring read_key restricts anonymous read/list. Configuring write_key restricts anonymous write. See the README for the full access matrix.


GET /health

Liveness probe. The handler does not probe storage — storage is always "ok" in this response shape (OpenAPI-compatible field only).

{"status":"ok","storage":"ok","version":"0.1.0-alpha"}
GET /ready

Checks whether metadata storage is reachable. On success (200): same shape as /health. On failure (503):

{"status":"degraded","storage":"error","version":"0.1.0-alpha"}
GET /metrics

Prometheus metrics in text exposition format.

GET /api-docs/openapi.json
GET /swagger-ui/

All bucket routes are under /api/v1.

POST /api/v1/
Content-Type: application/x-www-form-urlencoded

Form fields:

FieldRequiredDescription
emailYesAdmin label for the bucket
secret_keyNoAdmin credential
read_keyNoRead/list credential
write_keyNoWrite credential
signing_keyNoUsed only to mint temporary tokens
default_ttlNoDefault TTL in seconds (0 = no expiry; omitting applies the hosted default of 604800 s)

Response: bucket ID as text/plain.

Terminal window
curl -X POST http://127.0.0.1:8080/api/v1/ \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'email=owner@example.com' \
--data-urlencode 'secret_key=admin-secret' \
--data-urlencode 'signing_key=token-signing-secret'
GET /api/v1/{bucket}
Authorization: Bearer <secret_key>

Returns a public projection of the bucket policy. Secrets and key hashes are never included.

{
"default_ttl": 604800,
"has_secret_key": true,
"has_read_key": false,
"has_write_key": false,
"has_signing_key": true,
"signing_key_generation": 0,
"anonymous_access": {
"read": true,
"write": true,
"enumerate": true,
"delete": false
}
}
HEAD /api/v1/{bucket}
Authorization: Bearer <secret_key>

Returns 200 if the bucket exists and the credential has admin access, 404 otherwise.

PATCH /api/v1/{bucket}
Authorization: Bearer <secret_key>
Content-Type: application/json

Supported fields: secret_key, read_key, write_key, signing_key, default_ttl. Unknown fields are rejected with 400.

Patch semantics:

  • Field absent — leave unchanged
  • Field null — clear the value (not allowed for secret_key)
  • Field string/number — set or rotate
Terminal window
curl -X PATCH "http://127.0.0.1:8080/api/v1/$BUCKET" \
-H 'Authorization: Bearer admin-secret' \
-H 'Content-Type: application/json' \
-d '{"read_key":"new-read-secret","default_ttl":3600}'

Returns 204 No Content.

DELETE /api/v1/{bucket}
DELETE /api/v1/{bucket}/
Authorization: Bearer <secret_key>

Both forms are equivalent. Returns 204 No Content.

GET /api/v1/{bucket}/

Query parameters:

ParameterDefaultDescription
prefixOnly return keys with this prefix
limit10000Maximum number of results
skip0Keys to skip after ordering
reversefalseReverse lexicographic order
valuesfalseInclude stored values
formattextResponse format: text, json, or jsonl

Format can also be selected with the Accept header: application/json, application/x-ndjson, or text/plain.

Terminal window
curl "http://127.0.0.1:8080/api/v1/$BUCKET/?format=json&prefix=user:" \
-H 'Authorization: Bearer read-secret'
["user:1","user:2","user:42"]

With values (?values=true&format=json):

[["user:42",{"name":"Ana"}]]

All key routes are under /api/v1/{bucket}/{key}.

Keys are percent-encoded in the path. The server percent-decodes them before storage, so user%2F42 and user/42 refer to the same logical key. Clients should encode /, spaces, and other reserved characters.

Key limits: up to 128 bytes. Value limit: up to 16 KiB.

PUT /api/v1/{bucket}/{key}
POST /api/v1/{bucket}/{key} (primary method, PUT is an alias)

Optional query parameter: ?ttl=<seconds> — overrides the bucket default TTL.

Value type is inferred from Content-Type and the body:

  • Content-Type: application/json → stored as JSON
  • Content-Type: text/plain → stored as UTF-8 text
  • UTF-8 body that parses as a number → stored as Int64 or Float64
  • UTF-8 body that is valid JSON → stored as JSON
  • Otherwise → raw bytes
Terminal window
curl -X PUT "http://127.0.0.1:8080/api/v1/$BUCKET/counter?ttl=3600" \
-H 'Authorization: Bearer write-secret' \
-d '0'

The stored value is echoed in the response.

GET /api/v1/{bucket}/{key}

Returns the stored value with the appropriate Content-Type. Returns 404 if the key does not exist or has expired.

Terminal window
curl "http://127.0.0.1:8080/api/v1/$BUCKET/user:42" \
-H 'Authorization: Bearer read-secret'
HEAD /api/v1/{bucket}/{key}

Returns 200 if the key exists and has not expired. Body is empty, Content-Type matches GET.

DELETE /api/v1/{bucket}/{key}

Returns 204 No Content. Returns 404 if the key does not exist.


PATCH /api/v1/{bucket}/{key}

Optional query parameter: ?ttl=<seconds>.

The request body must be a signed decimal delta: +N or -N where N is an integer or float.

Terminal window
curl -X PATCH "http://127.0.0.1:8080/api/v1/$BUCKET/page-views" \
-H 'Authorization: Bearer write-secret' \
-d '+1'

Response body is the new counter value.

Type promotion rules:

  • Int64 + Int64Int64
  • Any float operand → Float64
  • Applying a counter delta to a missing key creates the key with the delta as its initial value
  • NaN, Inf, or overflow → 400

Tokens allow scoping credentials to a key prefix and a set of permissions.

POST /api/v1/{bucket}/tokens/
Authorization: Bearer <secret_key>
Content-Type: application/x-www-form-urlencoded

The bucket must have a signing_key configured. Returns 503 if it does not.

Form fields:

FieldRequiredDescription
prefixYesNon-empty key prefix the token is restricted to
permissionsYesComma-separated: read, write, enumerate, delete
ttlYesToken lifetime in seconds (must be > 0)
Terminal window
curl -X POST "http://127.0.0.1:8080/api/v1/$BUCKET/tokens/" \
-H 'Authorization: Bearer admin-secret' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'prefix=user:42:' \
--data-urlencode 'permissions=read,write,enumerate' \
--data-urlencode 'ttl=900'

Response:

{"access_token":"<jwt>"}

Use the token as a Bearer credential:

Terminal window
curl "http://127.0.0.1:8080/api/v1/$BUCKET/user:42:profile" \
-H 'Authorization: Bearer <jwt>'

Tokens are restricted to their configured prefix and permissions. A token scoped to user:42: cannot access admin:config. Rotating the bucket signing_key invalidates all previously issued tokens.


Atomic multi-key operations.

POST /api/v1/{bucket}
Authorization: Bearer <credential>
Content-Type: application/json

Request body:

{
"txn": [
{ "set": "profile:name", "value": "Ada", "ttl": 3600 },
{ "delete": "profile:old-name" }
]
}

Each operation is one of:

  • { "set": "<key>", "value": <json>, "ttl": <seconds> }ttl is optional
  • { "delete": "<key>" }

Value rules for set:

  • JSON strings are stored as UTF-8 text
  • Numbers, booleans, arrays, and objects are stored as JSON
  • null is rejected with 400
Terminal window
curl -X POST "http://127.0.0.1:8080/api/v1/$BUCKET" \
-H 'Authorization: Bearer write-secret' \
-H 'Content-Type: application/json' \
-d '{"txn":[{"set":"k1","value":"hello"},{"delete":"k2"}]}'

Returns 204 No Content on success. All operations either commit together or none do.


All errors use a consistent JSON envelope:

{
"error": {
"code": 404,
"message": "not_found"
}
}

Common messages:

MessageHTTP status
bad_request400
unauthorized401
forbidden403
not_found404
not_acceptable406
method_not_allowed405
service_unavailable503
internal_error500