API Reference
The kubenest REST API is the interface used by the web console, CLI tools, and any automation you build on top of kubenest. Every action the UI performs is an API call — there is no privileged back channel. This page covers the conventions that apply across the API and indexes the public application and platform endpoints.
Swagger UI at /docs and the OpenAPI document at /openapi.json are available without authentication in local environments and to superusers in staging. Production deployments do not expose either route, so use this page as the production reference.
Base URL
All endpoints are prefixed with:
https://api.{your-domain}/api/v1Replace {your-domain} with the domain you configured during installation. If you are running a local development instance, the default is http://localhost:8000/api/v1.
Authentication
Obtaining tokens
Authenticate by posting credentials to the login endpoint. Note that the login endpoint uses application/x-www-form-urlencoded (not JSON) to comply with the OAuth2 password flow convention:
curl -X POST https://api.your-domain.com/api/v1/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin@example.com&password=your-password" \
-c cookies.txtResponse:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer"
}The login response also sets an HttpOnly refresh token cookie. Do not discard it — you need it to refresh the access token.
Using the access token
For an endpoint that requires authentication, include the access token in the Authorization header:
Authorization: Bearer {access_token}Access tokens expire after 30 minutes by default; deployments can override that setting. An invalid or expired bearer token returns 401 Unauthorized with detail: "User not authenticated."; refresh the token (see below).
Refreshing the access token
The refresh endpoint reads the HttpOnly cookie set at login and issues a new access token without requiring you to re-enter your password. It does not rotate the refresh cookie:
curl -X POST https://api.your-domain.com/api/v1/refresh \
-b cookies.txtIn a browser context, the browser sends the cookie automatically. In a script, save it at login with -c and send it on refresh and logout with -b.
Refresh tokens expire after 7 days by default; deployments can override that setting. After a refresh token expires, the user must log in again.
Logging out
curl -X POST https://api.your-domain.com/api/v1/logout \
-H "Authorization: Bearer $TOKEN" \
-b cookies.txtLogout requires both the bearer access token and the refresh-token cookie. It blacklists both tokens and clears the refresh cookie.
Request conventions
Content-Type. JSON request bodies use Content-Type: application/json. POST /login uses application/x-www-form-urlencoded; POST /stack-templates/from-yaml uses multipart/form-data. Requests without a body do not need a content type.
Namespaced resources. Apps and stack templates are addressed by {namespace}/{name} rather than by UUID. The namespace maps to the project’s Kubernetes namespace, and the name is the App or template name you chose at create time. This makes URLs human-readable and stable.
Pagination. Paginated endpoints return pagination fields beside the data array:
{
"data": [...],
"total_count": 142,
"page": 1,
"items_per_page": 10
}Pass ?page=2&items_per_page=50 where pagination is implemented. Most paginated endpoints default to 10 items and cap a page at 100; addon revision history defaults to 50 and caps at 200. Some list endpoints return only data and total_count.
Filtering. Filters are endpoint-specific. Unsupported query parameters are not a portable way to filter a response; use only the parameters listed for that route.
Response conventions
Success codes. Status codes are route-specific. Reads and updates generally return 200 OK, creates generally return 201 Created, and deletes that have no response body return 204 No Content. Cluster scale and provisioning-job retry return 202 Accepted because the infrastructure work continues asynchronously.
Asynchronous reconciliation. App and addon creates can return 201 Created, and app/addon updates can return 200 OK, before cluster reconciliation finishes. Track the resource phase through its read endpoint or SSE stream; do not use 202 alone as the signal that work is asynchronous.
Error format
4xx client errors use the FastAPI detail field. Many routes return a string:
{ "detail": "App 'my-app' not found in namespace 'my-project'" }422 Unprocessable Entity (validation failures) returns a list of structured errors, one per invalid field:
{
"detail": [
{
"loc": ["body", "components", 0, "workload_spec", "replicas"],
"msg": "ensure this value is greater than 0",
"type": "value_error.number.not_gt"
}
]
}The loc array identifies where validation failed, such as "body", "query", "path", or "cookie".
Some routes put a structured object in detail; for example, App 409 Conflict responses include a machine-readable code and context when a resource is in blocked_sync drift state or a component removal would break an exportRef dependency.
Hub- and operator-dependent endpoints can return 502, 503, or 504 when dispatch or the response fails. The exact status and detail are endpoint-specific.
Rate limits
The API is not rate limited, with two exceptions. Sign-in is throttled: POST /login counts attempts per submitted email and source address, and per source address; after 20 attempts within a minute, further attempts are refused with the same 401 a wrong password gets, for a window that doubles with persistence up to 15 minutes. A correct password clears its own count, and nothing locks an account permanently. One task endpoint is throttled: configurable per-tier, per-path limits are enforced only on POST /tasks/task, which requires an authenticated caller; with the defaults it allows ten requests per hour per caller. Every other endpoint is unthrottled. There is no endpoint for reading current usage, and responses carry no X-RateLimit-* headers — do not build a client that waits for them.
What exists is the configuration surface. A superuser can define per-tier, per-path limits:
| Method | Path | Description |
|---|---|---|
POST | /tier/{tier_name}/rate_limit | Define a limit for a tier. Superuser only. Required body fields: path, limit (requests), and period (seconds); name is optional. |
GET | /tier/{tier_name}/rate_limits | List a tier’s configured limits. |
GET | /tier/{tier_name}/rate_limit/{id} | Get one configured limit. |
PATCH | /tier/{tier_name}/rate_limit/{id} | Update a configured limit. Superuser only. |
DELETE | /tier/{tier_name}/rate_limit/{id} | Remove a configured limit. Superuser only. |
Endpoint index
Control plane version
Which control plane you are talking to, in a form you can compare.
| Method | Path | Description |
|---|---|---|
GET | /version | Returns contract and build. contract is a monotonic integer that increases when client-visible behaviour changes — this is the field to compare when you need to know whether a fix is present. build is the git SHA of the running image, for support and forensics; it is null when the image was not stamped at build time. Accepts a user JWT or a CLI token with bundles:read. |
contract is deliberately not a semantic version and carries no compatibility promise. It says one thing: whether this control plane is at or after the point where some behaviour changed. Where a fix or an advisory records a floor, it records that floor as a contract number, so checking it is a single integer comparison and needs nothing but the response:
CONTRACT=$(curl -fsS -H "Authorization: Bearer $TOKEN" \
"https://api.your-domain.com/api/v1/version" | jq -r .contract)
[ "$CONTRACT" -ge 1 ] && echo "at or after contract 1"Do not compare build values. Two commit SHAs cannot be ordered without the repository they came from, which is why the orderable field is separate.
A control plane that answers 404 here predates the endpoint. That means the check is unavailable, not that the fix is absent — do not read a missing answer as a negative one.
Auth
| Method | Path | Description |
|---|---|---|
POST | /login | Obtain access token. Body: application/x-www-form-urlencoded with username and password. Sets refresh cookie. |
POST | /refresh | Exchange the refresh cookie for a new access token. The refresh cookie is not rotated. |
POST | /logout | Blacklist the bearer and refresh tokens and clear the refresh cookie. Requires both tokens. |
Clusters
| Method | Path | Description |
|---|---|---|
GET | /orgs/{org_id}/clusters | List all clusters in the organization. |
POST | /orgs/{org_id}/clusters | Register a new cluster or initiate cloud provisioning. |
GET | /clusters/{id} | Get cluster details, status, and metrics. |
PATCH | /clusters/{id} | Update cluster metadata (name, description). |
DELETE | /clusters/{id} | Delete the cluster. Requires the org admin role. Returns 409 while any project still exists on the cluster, naming the blocking projects; there is no force or cascade parameter. Once the cluster is empty, cloud-provisioned clusters get a Terraform destroy of their infrastructure and BYOC records are deleted immediately. |
GET | /clusters/{id}/metrics | Prometheus cluster-capacity time series queried through the operator and hub. Optional query: ?range=1h. |
GET | /clusters/{id}/install-instructions | Secret-free connection status: a runnable existing-cluster CLI command when one is supported (currently null), hub URL, namespace, pinned operator chart reference and a docs link. Contains no token, key or other credential — this is everything the console may render. (GET /clusters/{id}/install-command is removed and returns 410 Gone: it embedded the cluster JWT and the Git token in a browser-renderable response.) |
POST | /clusters/{id}/agent-credentials | Mint the cluster’s install-time credentials — the versioned agent JWT and, when GitOps is configured, a write deploy key on this cluster’s own Git repository. Secrets are returned exactly once, in this response, and are never retrievable again. Calling again re-mints: the JWT’s token_version increments and superseded deploy keys are deleted after a grace window. Re-minting does not revoke — the previously issued JWT keeps working, which is what makes re-running an install safe. To revoke, use rotate-token below. Accepts a user JWT; a CLI token must carry clusters:register. Audit-logged. |
POST | /clusters/{id}/rotate-token | Revoke this cluster’s agent JWT. Mints a new one, raises the cluster’s revocation floor to it, and closes the cluster’s live hub session; every previously issued token for the cluster is refused from that moment. Returns the new token — the cluster stays disconnected until you deploy it via an install or upgrade, so this is not a routine hygiene operation. enforcement is in_force when the hub confirmed the new floor, or pending when it did not answer; pending still means the rotation happened, so do not retry it. Requires a user JWT — a clusters:register CLI token cannot revoke. Audit-logged. |
POST | /clusters/{id}/install-events | Report one live platform install or upgrade stage transition. Accepts a user JWT; a CLI token must carry install:report. The body is the canonical journal entry: an accepted install or upgrade stage name, status (started, completed, or failed), optional manifest component key, optional user-safe detail, and a machine-readable reason_code required on failure. The backend relays every transition over SSE, persists terminal transitions in the install journal, and drives installing / install_failed. Never put secrets or raw command output in this body. |
POST | /clusters/{id}/scale | Change node count (cloud-provisioned clusters only). Requires the org admin role — it runs a Terraform apply that adds or removes real machines. |
GET | /clusters/{id}/bundle | Get the cluster’s bundle + profile record: bundle version, profile set, HA tier, volume-group ownership and install journal. All fields are null until a KubeNest Platform bundle install records them. Accepts a user JWT or a CLI token with clusters:read. |
PUT | /clusters/{id}/bundle | Write the bundle record (the installer’s record stage). Validated against the bundle catalog: unknown bundle versions, unknown profile names and HA tiers the bundle does not offer are rejected with 422. Accepts a user JWT or a CLI token with install:report. Replaces the removed GET/PUT /clusters/{id}/config component-toggle endpoints. |
GET | /clusters/{id}/provisioning-jobs | List provisioning jobs for a cloud-provisioned cluster, with status and timestamps. |
GET | /clusters/{id}/rbac | List cluster-scoped role assignments. |
POST | /clusters/{id}/rbac | Grant a cluster-scoped role to a user. |
PUT | /clusters/{id}/rbac | Replace a user’s cluster-scoped role. |
DELETE | /clusters/{id}/rbac | Revoke a cluster-scoped role assignment. Required query: ?binding_id=<UUID>. |
GET | /clusters/{id}/registry-secrets | List image pull secrets attached at the cluster level. |
POST | /clusters/{id}/registry-secrets | Attach an image pull secret at the cluster level. |
Bundles
The bundle catalog: the KubeNest Platform release manifests this control plane knows about. Manifests are authored in kubenest-contracts/bundles/ and validated against bundle-manifest.schema.json; the backend serves them read-only.
| Method | Path | Description |
|---|---|---|
GET | /bundles | List available bundle versions with their HA tiers and profile names, oldest first. |
GET | /bundles/{version} | One bundle manifest, exactly as authored: component pins, OS support matrix, HA tiers, limits, upgrade tooling, backup defaults and profiles. |
GET | /bundles/{version}/diff/{other} | Diff two bundle versions: per-component from/to for core and each shared profile, added/removed profiles, OS and HA-tier changes, and changed limit/upgrade/backup settings as dotted paths. |
Projects
| Method | Path | Description |
|---|---|---|
GET | /projects | List all projects the user can access. |
POST | /projects | Request creation of a project (Kubernetes namespace) on a cluster. The backend persists the desired project before dispatching it through the hub; the initial lifecycle is pending, not a claim that the namespace already exists. It becomes ready only after the operator reports the matching Project. If the hub or cluster cannot confirm it, the record stays visible with a named degraded lifecycle reason and reconciliation continues. |
GET | /projects/{id} | Get project details and status. |
DELETE | /projects/{id} | Request project cleanup. Requires the org admin role and returns 202 Accepted with a retained cleanup-pending record. The backend keeps that record until the operator confirms that both the managed Project CR and its namespace (including its workloads, addons, and secrets) are absent. Persistent-volume retention follows the cluster storage class policy. If the cluster cannot confirm cleanup, the record stays visible with a cleanup_degraded lifecycle reason and reconciliation continues; it is never reported as deleted merely because delivery to the cluster failed. |
GET | /projects/{id}/apps | List every app in the project. |
GET | /projects/{id}/available-exports | List export keys published by standalone addon instances in the project. |
GET | /projects/{id}/rbac | List project-scoped role assignments. |
POST | /projects/{id}/rbac | Grant a project-scoped role to a user. |
PUT | /projects/{id}/rbac | Replace a user’s project-scoped role. |
DELETE | /projects/{id}/rbac | Revoke a project-scoped role assignment. Required query: ?binding_id=<UUID>. |
GET | /projects/{id}/secrets-overview | Summarize the secrets visible to the project and where each one is inherited from. |
GET | /projects/{id}/registry-secrets | List image pull secrets attached at the project level. |
POST | /projects/{id}/registry-secrets | Attach an image pull secret at the project level. |
GET | /projects/{id}/effective-registry-secrets | Resolve the image pull secrets that actually apply, merging cluster-level and project-level attachments. |
Projects have no update endpoint — there is no PATCH /projects/{id}. Name and cluster are fixed at creation; changing them means deleting the project and creating a new one.
kubenest does not manage ResourceQuota. A project is a namespace boundary, not a capacity boundary: no quota field is accepted at creation, and nothing is applied to the namespace that caps what its workloads can consume.
Apps
Every route below that contains {namespace}/{name} requires ?project_id=<UUID>, including reads, mutations, logs, metrics, addon attachment, deployment history, and rollback. Omitting it returns 422 Unprocessable Entity.
| Method | Path | Description |
|---|---|---|
GET | /apps | List apps. Optional filter: ?project_id=. |
POST | /apps | Create an app. Body includes name, project_id, and components array. Returns 201 Created. |
GET | /apps/{namespace}/{name} | Get app details, component specs, phase, and drift state. |
PATCH | /apps/{namespace}/{name} | Add, remove, or patch components. Returns 200 OK. |
DELETE | /apps/{namespace}/{name} | Delete the app and its components. Returns 204 No Content. |
POST | /apps/{namespace}/{name}/scale | Scale a specific workload component’s replica count. Body: {component_name, replicas}. |
POST | /apps/{namespace}/{name}/pause | Set all workload replicas to 0, snapshot pre-pause counts. |
POST | /apps/{namespace}/{name}/resume | Restore pre-pause replica counts. |
POST | /apps/{namespace}/{name}/redeploy | Force a hard-refresh and re-sync without spec changes. |
GET | /apps/{namespace}/{name}/deployments | List App operation history. |
POST | /apps/{namespace}/{name}/rollback | Restore a prior deployment spec. Body: exactly one of {deployment_id} or {revision}. Returns 200 OK. |
POST | /apps/{namespace}/{name}/deployments/{deployment_id}/rollback | Roll back to one specific deployment by ID. |
GET | /apps/{namespace}/{name}/logs | Stream logs as SSE. A multi-workload app also requires ?component=<name>. |
GET | /apps/{namespace}/{name}/components/{component}/logs/stream | Stream logs from a single component. |
GET | /apps/{namespace}/{name}/metrics | App, workload-component, and cluster-capacity Prometheus time series. Optional query: ?range=1h. |
POST | /apps/{namespace}/{name}/attach-addon | Inject an existing addon instance’s name as an exportRef. This does not add an addon component; without an existing same-name component, operator validation fails. |
DELETE | /apps/{namespace}/{name}/attach-addon/{addon_instance_id} | Detach an addon instance from the app. |
GET | /apps/{namespace}/{name}/components/{component}/secrets | List a component’s secret keys. |
PATCH | /apps/{namespace}/{name}/components/{component}/secrets | Set or update a component’s secret values. |
DELETE | /apps/{namespace}/{name}/components/{component}/secrets/{key} | Remove a single secret key from a component. |
Stack Templates
| Method | Path | Description |
|---|---|---|
GET | /stack-templates | List templates. Optional filters: ?namespace= and ?scope=. |
POST | /stack-templates | Create a template directly (full template body). |
GET | /stack-templates/{namespace}/{name} | Get template details including parameters and component specs. |
PUT | /stack-templates/{namespace}/{name} | Update description, icon, tags, components, or parameters in place. The version is unchanged. |
DELETE | /stack-templates/{namespace}/{name} | Delete a template. The backend does not check for deployed instances that reference it. |
POST | /stack-templates/from-app/{namespace}/{name} | Capture a running app as a new template. Required query: ?project_id=<UUID>. Body includes name, version, scope, and preserve_export_refs. |
POST | /stack-templates/from-chart | Create a template wrapping a Helm chart with promoted parameters. |
POST | /stack-templates/from-yaml | Validate YAML supplied as multipart file or yaml_content. It validates only by default; add ?create=true&namespace=... to create the template. |
POST | /stack-templates/{namespace}/{name}/deploy | Deploy a template to a project. Body: {project_id, parameters, timeout}. Returns 201 Created with message, deploy_name, and namespace. |
GET | /stack-templates/inspect-chart | Fetch schema and defaults for a Helm chart. Query params: ?repo=&name=&version=. Responses are cached for 1 hour when Redis is available. |
GET | /stack-templates/registry | Browse the community template registry. |
Addon Instances
| Method | Path | Description |
|---|---|---|
GET | /addon-instances | List addon instances. At least one of ?project_id= or ?org_id= is required. Supports page and items_per_page. |
POST | /addon-instances | Deploy a standalone addon instance. Returns 201 Created. |
GET | /addon-instances/{id} | Get instance details, current phase, and exports. Secret-backed values are redacted; the response includes their keys and Secret reference. |
PATCH | /addon-instances/{id} | Update exactly one of values or chart_version. Creates a new revision and returns 200 OK. |
DELETE | /addon-instances/{id} | Uninstall and delete the addon instance. No parameters. Returns 502 without deleting the record if the hub is unreachable. |
POST | /addon-instances/{id}/rollback | Restore a prior revision. Body must contain exactly one of {revision_number} or {revision_id}. |
GET | /addon-instances/{id}/revisions | List all revisions with values snapshots and notes. |
GET /projects/{id}/available-exports lists the keys published by standalone addon instances. It does not report reverse dependencies or App-component exports.
Addon Definitions
| Method | Path | Description |
|---|---|---|
GET | /addon-definitions | List all addon definitions in the catalog. |
GET | /addon-definitions/{id} | Get a definition by UUID, including its chart reference, default values, and export schema. |
Organizations and users
Organizations are the top-level tenant boundary. Clusters, projects, and credentials all belong to one.
| Method | Path | Description |
|---|---|---|
GET | /orgs | List organizations the caller belongs to. |
POST | /orgs | Create an organization. Superuser only. |
GET | /orgs/{id} | Get organization details. |
PUT | /orgs/{id} | Update an organization. Org admin only. |
DELETE | /orgs/{id} | Delete an organization. Superuser only. |
GET | /orgs/{id}/members | List members and their organization roles. Requires org membership. |
POST | /orgs/{id}/members | Add a user to the organization. Org admin only. |
PUT | /orgs/{id}/members/{user_id} | Change a member’s organization role. Org admin only. |
DELETE | /orgs/{id}/members/{user_id} | Remove a member from the organization. Org admin only. |
GET | /org/settings | Get settings for ?org_id=<UUID>. The query parameter is required. |
PATCH | /org/settings | Update settings for ?org_id=<UUID>. The query parameter and org admin role are required. |
GET | /orgs/{id}/credentials | List cloud provider credentials used for cluster provisioning. |
POST | /orgs/{id}/credentials | Store a new set of cloud provider credentials. |
GET | /credentials/{id} | Get a credential record. Secret material is never returned. |
PATCH | /credentials/{id} | Rotate or rename a credential. |
DELETE | /credentials/{id} | Delete a credential. |
GET | /user/me | Get the authenticated user’s profile and role assignments. |
GET | /users | List users. |
POST | /user | Create a user. |
GET | /user/{id} | Get a user. |
PATCH | /user/{id} | Update the authenticated user’s own record. |
DELETE | /user/{id} | Delete the authenticated user’s own record. |
Provisioning jobs
Cloud cluster provisioning runs asynchronously. Creating a cluster with a cloud provider returns a job you poll or watch over SSE.
| Method | Path | Description |
|---|---|---|
GET | /provisioning-jobs/{id} | Get job status and current phase. |
GET | /provisioning-jobs/{id}/logs | Fetch Terraform and bootstrap output for the job. |
POST | /provisioning-jobs/{id}/retry | Retry a failed job without recreating resources that already exist. |
Events (SSE)
| Method | Path | Description |
|---|---|---|
GET | /events/stream | Server-Sent Events stream. Optional filters: cluster_id, project_id, workload_id, and resource_type (cluster, project, workload, build, addon, app, activity, or all). |
The SSE stream uses standard text/event-stream encoding. Each frame has an event name and a JSON data payload. install_stage_update carries the same install journal entry sent by the CLI: started makes the current stage move immediately, while completed and failed are terminal. A failed component stage includes the exact component key and puts the cluster in install_failed; the same stage and component remain readable from GET /clusters/{id}/bundle even after the live event has passed. Other event types include app, addon, cluster-status, drift, and build updates.
curl -N -H "Authorization: Bearer $TOKEN" \
"https://api.your-domain.com/api/v1/events/stream?project_id=$PROJECT_ID&resource_type=app"The connection is long-lived. Clients should reconnect with backoff if it drops. The endpoint does not implement Last-Event-ID replay; after reconnecting, refresh current state through the relevant read endpoint.
See also:
- Architecture Overview — how the API, hub, and operator relate
- Creating and Managing Apps — practical walkthroughs of the most common app API calls
- Managing Addons — addon instance API calls with real examples
- Local and staging OpenAPI UI:
/docs