Skip to main content

External API (Beta)

Embedded Cluster exposes a small, stable HTTP API on every controller node for cross-node automation. This API is intended for headless installs, programmatic node joins, and external orchestration.

Overview​

  • The API listens on TCP port 30000 by default, the same port that serves the admin console. Override at install with --api-port. The port cannot be changed after installation.
  • All traffic is TLS-encrypted using the certificates configured at install time and is served by the daemon.
  • All endpoints except health and version require a JWT bearer token obtained from POST /api/v1/auth/token.
  • The daemon listens on all network interfaces, so the API is reachable from any host that can reach the port. Use a firewall to limit which hosts can connect.

The full Embedded Cluster API (used by the local web UI) is served on a Unix socket and is not exposed on the network. Only the endpoints documented on this page are supported for automation.

In Embedded Cluster versions earlier than 3.17.0-beta.1, the API listens on port 30081 by default and serves these endpoints under /v1 instead of /api/v1, with the health endpoint at /v1/health. When you upgrade a cluster that uses the default port to 3.17.0-beta.1 or later, the API moves to port 30000. A cluster installed with a custom --api-port keeps that port.

Authentication​

Obtain a token using the installer password:

curl -X POST https://<controller-ip>:30000/api/v1/auth/token \
-H 'Content-Type: application/json' \
-d '{"password":"<installer-password>"}'

Response:

{ "token": "eyJhbGciOi..." }

The token is a JWT valid for 24 hours. Pass it as Authorization: Bearer <token> on subsequent requests.

Endpoints​

GET /api/v1/healthz​

Liveness probe. No authentication required.

curl https://<controller-ip>:30000/api/v1/healthz

Response:

{ "status": "ok" }

GET /api/v1/version​

Returns the running daemon version. No authentication required.

curl https://<controller-ip>:30000/api/v1/version

Response:

{
"version": "3.0.0",
"commit": "abcdef0",
"buildDate": "2026-01-15_12:00:00"
}

POST /api/v1/auth/token​

See Authentication.

GET /api/v1/nodes​

Returns the list of nodes in the cluster.

Request:

curl https://<controller-ip>:30000/api/v1/nodes \
-H "Authorization: Bearer $TOKEN"

Response:

{
"nodes": [
{
"name": "controller-0",
"labels": {
"node-role.kubernetes.io/control-plane": "",
"kots.io/embedded-cluster-role-0": "controller"
},
"annotations": { ... },
"roles": ["controller"],
"internalIP": "10.0.0.11",
"ready": true
}
]
}

Field reference:

FieldDescription
nameKubernetes node name.
labelsAll labels on the node.
annotationsAll annotations on the node.
rolesThe vendor-defined role names assigned at join time (from the EC config spec.roles).
internalIPThe node's internal IP address.
readytrue when the node's Ready condition is True.

Automation should poll this endpoint to verify the cluster is ready to issue join commands. After the k0s install completes, the controller takes a short time to report Ready in the cluster. Calls to POST /api/v1/create-join-command fail until at least one controller has ready: true, so automation should poll GET /api/v1/nodes first and only request join commands once a Ready controller is present.

POST /api/v1/create-join-command​

Returns the shell commands a remote host must run to join the cluster: download a join bundle, extract it, and run node join. The download command embeds a short-lived bearer token (1 hour TTL).

Request:

curl -X POST https://<controller-ip>:30000/api/v1/create-join-command \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"roles":["controller"]}'

The roles array must contain one or more role names defined in the application's EC config (spec.roles.controller.name and spec.roles.custom[].name). Multiple roles can be assigned to the same node; the joined node will carry the union of each role's labels.

Response:

{
"tokenExpiresAt": "2026-01-15T13:00:00Z",
"joinSteps": [
{
"description": "Download the join bundle",
"command": "curl -f -k --pinnedpubkey \"sha256//...\" -H \"Authorization: Bearer ...\" \"https://10.0.0.11:30000/api/v1/join-bundle?roles=controller\" -o APP_SLUG-join-20260115-120000.tar.gz"
},
{
"description": "Extract the bundle",
"command": "tar xzf APP_SLUG-join-20260115-120000.tar.gz"
},
{
"description": "Run the join command",
"command": "sudo ./APP_SLUG node join"
}
]
}

GET /api/v1/join-bundle?roles=<role1,role2,...>​

Streams a gzip tarball containing everything a new node needs to join the cluster: binaries, EC bundle, license, signed cluster seed, and a k0s join token. The roles query parameter accepts a comma-separated list of role names.

curl https://<controller-ip>:30000/api/v1/join-bundle?roles=controller \
-H "Authorization: Bearer $TOKEN" \
-o APP_SLUG-join-controller.tar.gz

Typically you do not call this endpoint directly. The curl command returned by POST /api/v1/create-join-command calls it for you with a token that expires after 1 hour. That token is not limited to this endpoint, so treat the returned commands as secrets and do not log them.

The request requires a multi-node-enabled license.

See also​

  • Headless joins in Access and manage embedded clusters — end-to-end walkthrough that composes these endpoints to join a node without the web UI.
  • roles in Embedded Cluster Config — declare the role names used in POST /api/v1/create-join-command and GET /api/v1/join-bundle.