Skip to Content
API Reference

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/v1

Replace {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:

login
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.txt

Response:

{ "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:

refresh token
curl -X POST https://api.your-domain.com/api/v1/refresh \ -b cookies.txt

In 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

logout
curl -X POST https://api.your-domain.com/api/v1/logout \ -H "Authorization: Bearer $TOKEN" \ -b cookies.txt

Logout 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:

MethodPathDescription
POST/tier/{tier_name}/rate_limitDefine a limit for a tier. Superuser only. Required body fields: path, limit (requests), and period (seconds); name is optional.
GET/tier/{tier_name}/rate_limitsList 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.

MethodPathDescription
GET/versionReturns 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:

is this control plane at or after the fix
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

MethodPathDescription
POST/loginObtain access token. Body: application/x-www-form-urlencoded with username and password. Sets refresh cookie.
POST/refreshExchange the refresh cookie for a new access token. The refresh cookie is not rotated.
POST/logoutBlacklist the bearer and refresh tokens and clear the refresh cookie. Requires both tokens.

Clusters

MethodPathDescription
GET/orgs/{org_id}/clustersList all clusters in the organization.
POST/orgs/{org_id}/clustersRegister 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}/metricsPrometheus cluster-capacity time series queried through the operator and hub. Optional query: ?range=1h.
GET/clusters/{id}/install-instructionsSecret-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-credentialsMint 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-tokenRevoke 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-eventsReport 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}/scaleChange 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}/bundleGet 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}/bundleWrite 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-jobsList provisioning jobs for a cloud-provisioned cluster, with status and timestamps.
GET/clusters/{id}/rbacList cluster-scoped role assignments.
POST/clusters/{id}/rbacGrant a cluster-scoped role to a user.
PUT/clusters/{id}/rbacReplace a user’s cluster-scoped role.
DELETE/clusters/{id}/rbacRevoke a cluster-scoped role assignment. Required query: ?binding_id=<UUID>.
GET/clusters/{id}/registry-secretsList image pull secrets attached at the cluster level.
POST/clusters/{id}/registry-secretsAttach 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.

MethodPathDescription
GET/bundlesList 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

MethodPathDescription
GET/projectsList all projects the user can access.
POST/projectsRequest 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}/appsList every app in the project.
GET/projects/{id}/available-exportsList export keys published by standalone addon instances in the project.
GET/projects/{id}/rbacList project-scoped role assignments.
POST/projects/{id}/rbacGrant a project-scoped role to a user.
PUT/projects/{id}/rbacReplace a user’s project-scoped role.
DELETE/projects/{id}/rbacRevoke a project-scoped role assignment. Required query: ?binding_id=<UUID>.
GET/projects/{id}/secrets-overviewSummarize the secrets visible to the project and where each one is inherited from.
GET/projects/{id}/registry-secretsList image pull secrets attached at the project level.
POST/projects/{id}/registry-secretsAttach an image pull secret at the project level.
GET/projects/{id}/effective-registry-secretsResolve 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.

MethodPathDescription
GET/appsList apps. Optional filter: ?project_id=.
POST/appsCreate 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}/scaleScale a specific workload component’s replica count. Body: {component_name, replicas}.
POST/apps/{namespace}/{name}/pauseSet all workload replicas to 0, snapshot pre-pause counts.
POST/apps/{namespace}/{name}/resumeRestore pre-pause replica counts.
POST/apps/{namespace}/{name}/redeployForce a hard-refresh and re-sync without spec changes.
GET/apps/{namespace}/{name}/deploymentsList App operation history.
POST/apps/{namespace}/{name}/rollbackRestore a prior deployment spec. Body: exactly one of {deployment_id} or {revision}. Returns 200 OK.
POST/apps/{namespace}/{name}/deployments/{deployment_id}/rollbackRoll back to one specific deployment by ID.
GET/apps/{namespace}/{name}/logsStream logs as SSE. A multi-workload app also requires ?component=<name>.
GET/apps/{namespace}/{name}/components/{component}/logs/streamStream logs from a single component.
GET/apps/{namespace}/{name}/metricsApp, workload-component, and cluster-capacity Prometheus time series. Optional query: ?range=1h.
POST/apps/{namespace}/{name}/attach-addonInject 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}/secretsList a component’s secret keys.
PATCH/apps/{namespace}/{name}/components/{component}/secretsSet 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

MethodPathDescription
GET/stack-templatesList templates. Optional filters: ?namespace= and ?scope=.
POST/stack-templatesCreate 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-chartCreate a template wrapping a Helm chart with promoted parameters.
POST/stack-templates/from-yamlValidate 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}/deployDeploy a template to a project. Body: {project_id, parameters, timeout}. Returns 201 Created with message, deploy_name, and namespace.
GET/stack-templates/inspect-chartFetch 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/registryBrowse the community template registry.

Addon Instances

MethodPathDescription
GET/addon-instancesList addon instances. At least one of ?project_id= or ?org_id= is required. Supports page and items_per_page.
POST/addon-instancesDeploy 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}/rollbackRestore a prior revision. Body must contain exactly one of {revision_number} or {revision_id}.
GET/addon-instances/{id}/revisionsList 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

MethodPathDescription
GET/addon-definitionsList 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.

MethodPathDescription
GET/orgsList organizations the caller belongs to.
POST/orgsCreate 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}/membersList members and their organization roles. Requires org membership.
POST/orgs/{id}/membersAdd 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/settingsGet settings for ?org_id=<UUID>. The query parameter is required.
PATCH/org/settingsUpdate settings for ?org_id=<UUID>. The query parameter and org admin role are required.
GET/orgs/{id}/credentialsList cloud provider credentials used for cluster provisioning.
POST/orgs/{id}/credentialsStore 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/meGet the authenticated user’s profile and role assignments.
GET/usersList users.
POST/userCreate 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.

MethodPathDescription
GET/provisioning-jobs/{id}Get job status and current phase.
GET/provisioning-jobs/{id}/logsFetch Terraform and bootstrap output for the job.
POST/provisioning-jobs/{id}/retryRetry a failed job without recreating resources that already exist.

Events (SSE)

MethodPathDescription
GET/events/streamServer-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.

subscribe to project app events
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:

Last updated on