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 key | One-time — used only on POST /v1/enroll to register a device |
| Refresh token | Long-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.
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
/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
| 400 | Missing project |
| 401 | Invalid setup key |
/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
| 401 | Invalid or revoked refresh token |
/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
| 401 | Invalid refresh token |
/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
| 400 | Missing or invalid name |
| 401 | Invalid or missing admin token |
| 409 | Project already exists |
/v1/projects
List all project names. Requires the global admin token.
Response — 200 OK
{
"projects": ["payments-service", "auth-service"]
}
/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
}
/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" }
]
/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
| 401 | Invalid or missing access token |
| 404 | Secret not found |
/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
| 400 | Missing or empty value |
| 401 | Invalid or missing access token |
| 403 | Read-only token cannot set secrets |
/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
| 401 | Invalid or missing access token |
| 403 | Read-only token cannot delete secrets |
| 404 | Secret not found |
/health
Health check endpoint. No authentication required. Used by load balancers and monitoring.
Response — 200 OK
{
"status": "ok"
}