API Reference

All endpoints are served over HTTPS at https://keyctl-api.azurewebsites.net. The API is fully usable with curl alone — no CLI binary required.

Authentication

keyctl uses a three-tier credential model. Each credential is a Bearer token used at a different stage:

Setup keyOne-time — used only on POST /v1/enroll to register a device
Refresh tokenLong-lived, revocable — used on POST /v1/token and /v1/revoke
Access token (JWT)~30 min, minted per session — used on all secret endpoints

The flow is: exchange your one-time setup key for a refresh token (enroll), then exchange the refresh token for a short-lived access token (JWT), then use the access token to read/write secrets. The setup key does not work on secret endpoints.

Creating a project uses a global admin token configured on the API (KEYCTL_ADMIN_TOKEN). This is separate from per-project setup keys.

Use the API without the CLI

If you can't download or run the binary, you can do everything the CLI does with curl. This is the recommended fallback.

1. Enroll (setup key → refresh token)

curl -X POST https://keyctl-api.azurewebsites.net/v1/enroll \
  -H "Authorization: Bearer $SETUP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"project":"payments-service","deviceName":"my-laptop"}'
# => { "refreshToken": "payments-service.ab12...c9", "role": "admin", ... }

2. Get an access token (refresh token → JWT)

curl -X POST https://keyctl-api.azurewebsites.net/v1/token \
  -H "Authorization: Bearer $REFRESH_TOKEN"
# => { "accessToken": "eyJhbGci...", "expiresAt": "...", "role": "admin" }

3. Read a secret (with the access token)

curl https://keyctl-api.azurewebsites.net/v1/projects/payments-service/secrets/database-url \
  -H "Authorization: Bearer $ACCESS_TOKEN"
# => { "key": "database-url", "value": "postgres://..." }

4. Inject into your environment (no CLI)

# fetch a secret straight into an env var, then run your app
export DATABASE_URL=$(curl -s \
  https://keyctl-api.azurewebsites.net/v1/projects/payments-service/secrets/database-url \
  -H "Authorization: Bearer $ACCESS_TOKEN" | python3 -c 'import sys,json;print(json.load(sys.stdin)["value"])')

node server.js
Access tokens expire (~30 min). When one expires, repeat step 2 with your refresh token to mint a new one.
POST /v1/enroll

Exchange a one-time setup key for a device-bound refresh token. The setup key is used only here.

Authorization

Bearer = the project setup key.

Request body

{
  "project":    "payments-service",
  "deviceName": "my-laptop"          // optional
}

Response — 201 Created

{
  "refreshToken": "payments-service.ab12...c9",  // store securely
  "deviceId":     "ab12cd34ef56",
  "project":      "payments-service",
  "role":         "admin"
}

Error codes

400Missing project
401Invalid setup key
POST /v1/token

Exchange a refresh token for a short-lived JWT access token (~30 min).

Authorization

Bearer = the refresh token.

Response — 200 OK

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expiresAt":   "2026-07-06T11:48:58Z",
  "role":        "admin"
}

Error codes

401Invalid or revoked refresh token
POST /v1/revoke

Revoke a device by deleting its registration. Existing access tokens stop working once they expire (within ~30 min).

Authorization

Bearer = the refresh token being revoked.

Response — 204 No Content

Error codes

401Invalid refresh token
POST /v1/projects

Create a new project and receive its initial admin setup key. The key is returned exactly once and never stored in plaintext.

Authorization

Requires the global KEYCTL_ADMIN_TOKEN as the Bearer token.

Request body

{
  "name": "payments-service"
}

Response — 201 Created

{
  "project":  "payments-service",
  "setupKey": "3f8a2c1b..."   // save this — shown once only
}

Error codes

400Missing or invalid name
401Invalid or missing admin token
409Project already exists
GET /v1/projects

List all project names. Requires the global admin token.

Response — 200 OK

{
  "projects": ["payments-service", "auth-service"]
}
POST /v1/projects/{project}/rotate-key

Generate a new admin setup key and revoke all previous ones. After calling this endpoint, prior setup keys can no longer enroll.

Authorization

Bearer = an admin access token for the project.

Response — 200 OK

{
  "setupKey": "a91f4c2e..."   // new key — shown once only
}
GET /v1/projects/{project}/secrets

List all secret keys (names) for a project. Values are never included in list responses.

Authorization

Bearer = an access token (JWT). See Use the API without the CLI.

Response — 200 OK

[
  { "key": "database-url",      "updatedAt": "2026-07-06T09:00:00Z" },
  { "key": "stripe-secret-key", "updatedAt": "2026-07-05T14:30:00Z" }
]
GET /v1/projects/{project}/secrets/{key}

Retrieve a single secret value. Both read-only and admin access tokens can call this.

Authorization

Bearer = an access token (JWT).

Response — 200 OK

{
  "key":   "database-url",
  "value": "postgres://user:pass@host:5432/db"
}

Error codes

401Invalid or missing access token
404Secret not found
PUT /v1/projects/{project}/secrets/{key}

Create or update a secret value. Requires an admin access token.

Request body

{
  "value": "postgres://user:pass@host:5432/db"
}

Response — 200 OK

{
  "key":    "database-url",
  "status": "ok"
}

Error codes

400Missing or empty value
401Invalid or missing access token
403Read-only token cannot set secrets
DEL /v1/projects/{project}/secrets/{key}

Delete a secret. Requires an admin access token. In soft-delete enabled vaults, the secret enters a deleted state and can be purged separately.

Authorization

Bearer = an admin access token.

Response — 204 No Content

Error codes

401Invalid or missing access token
403Read-only token cannot delete secrets
404Secret not found
GET /health

Health check endpoint. No authentication required. Used by load balancers and monitoring.

Response — 200 OK

{
  "status": "ok"
}