API reference
The full reference of the user-facing REST API lives in the repository:
docs/API.md. Here is what you need to know before the first request.
Base and format
All paths are relative to /api/v1, format is JSON. Responses and error
codes are in English; the interface language is the client's concern.
Server liveness — GET /healthz (outside /api/v1).
Authentication
| Subject | Mechanism |
|---|---|
| Human | Authorization: Bearer <access token>, 15 minutes |
| Browser | the same header + refresh in an httpOnly cookie |
| Identity | its own token from /machine/login or /machine/enroll, scope "project × environment", TTL configurable from 60 to 86400 s |
The X-Client header (web, portal, cli, sdk, mobile) goes into
the audit log and decides where the refresh token ends up: for a browser
it is issued only in a cookie.
Machine routes:
| Route | What it does |
|---|---|
POST /machine/login | client ID + client secret → access token |
POST /machine/enroll | enrollment token + public key → permanent credentials |
GET /machine/entries?project=&env= | environment entries within the identity's scope |
POST /machine/heartbeat | "alive" mark in the identity's lastUsedAt |
All of this is covered in detail on the Services and identities page.
Errors
{ "error": { "code": "VALIDATION", "message": "..." } }
Branch on code, not on text. Every response carries an X-Request-Id —
that is the only thing you need to quote in a support request.
| HTTP | Code | When in practice |
|---|---|---|
| 400 | VALIDATION | body or parameters failed validation; the text says what exactly |
| 401 | UNAUTHENTICATED | no token, wrong token, invalid client ID / client secret, unknown enrollment token |
| 401 | TOKEN_EXPIRED | the access token expired; log in again |
| 402 | QUOTA_EXCEEDED | you hit the plan limit — not a permissions issue |
| 403 | FORBIDDEN | the role is insufficient, the IP is outside allowedCidrs, the identity scope is wrong |
| 403 | IDENTITY_REVOKED | the identity is revoked; its tokens are not accepted |
| 403 | ENROLLMENT_EXPIRED | the enrollment token is older than 10 minutes and was never used |
| 403 | ENROLLMENT_USED | the enrollment token has already been used — an alert, see identities |
| 403 | ORG_BLOCKED | the organization is blocked by the operator |
| 404 | NOT_FOUND | no such resource (or no rights to see it) |
| 409 | CONFLICT | slug taken, identity already revoked, nothing to roll back |
| 413 | PAYLOAD_TOO_LARGE | the request body exceeds the limit (1 MiB by default) |
| 429 | RATE_LIMITED | too frequent; wait for Retry-After |
| 500 | INTERNAL | a server-side error; quote the X-Request-Id in your support request |
Rate limits are on the Security page.
Pagination
{ "items": [...], "nextCursor": "..." }. The cursor is opaque: pass it
back in ?cursor= for as long as it comes.
Rotation
A policy is attached to an entry; runs happen on schedule in a background
worker or are triggered out of schedule. Phases, strategies and the
pending/current labels are covered on the
Secret rotation page.
| Method | Path | Role |
|---|---|---|
| PUT | /entries/:id/rotation-policy | project admin |
| GET | /entries/:id/rotation-policy | project viewer |
| DELETE | /entries/:id/rotation-policy | project admin |
| POST | /rotation-policies/:id/run-now | project admin |
| GET | /rotation-runs?policyId=<id> | project viewer |
| GET | /orgs/:id/rotations | organization member |
PUT body:
{
"rotator": "postgres",
"strategy": "dual-credential",
"intervalSeconds": 604800,
"config": {
"adminDsnEntryRef": "my-org/billing-api/production/DB_ADMIN_DSN",
"usernames": ["app_a", "app_b"]
},
"enabled": true
}
rotator— currently onlypostgres;strategy—dual-credentialorsingle-user;intervalSeconds— an integer, no less than 60;config— parsed by the rotator; secret references in it must live in the same organization, otherwiseVALIDATION, and you must hold a role of at leastmemberin each referenced secret's project, otherwiseFORBIDDEN.PUTreplacesconfigwholesale: when editing, resend theactiveUsernamefromGET, otherwise the rotator starts from the first user in the list;enabled— optional, defaults totrue.
PUT creates a policy or replaces the existing one and returns it:
{ id, entryId, rotator, strategy, intervalSeconds, config, enabled, nextRotationAt, createdAt }. GET returns the entry's policy, DELETE
returns 204; both answer NOT_FOUND when there is no policy.
POST /rotation-policies/:id/run-now starts a run immediately and returns
it. If a run of this policy is already in progress, the response is
409 CONFLICT: the manual trigger and the background worker share one
policy lease. A paused (enabled: false) policy still runs — this is an
explicit request, not a schedule; every such run is written to the audit
log as a rotation.run.requested event.
GET /rotation-runs?policyId=<id> returns { items } with the
policy's latest runs (up to 50, newest first). A run:
{ id, policyId, phase, status, error?, resultVersion?, startedAt, completedAt? }, where
phase is one of create, set, test, finish, and status is
running, testing, succeeded or failed; the error text is present
only on failed, and resultVersion is the number of the entry version
the run created (present once the run has written a value).
GET /orgs/:id/rotations is the organization-wide summary: up to 5 latest
runs per policy, 50 in total, across the projects where you have a role
(the organization owner and admins see every project). Each element
additionally carries entryKey — the entry's key name.
A common mistake
GET /projects requires ?orgId= — without it the response will be
VALIDATION.