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:
| Field | Description |
|---|---|
name | Kubernetes node name. |
labels | All labels on the node. |
annotations | All annotations on the node. |
roles | The vendor-defined role names assigned at join time (from the EC config spec.roles). |
internalIP | The node's internal IP address. |
ready | true 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-commandandGET /api/v1/join-bundle.