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 the controller's internal IP on TCP port 30081 by default. Override at install with
--api-port. - All traffic is TLS-encrypted using the certificates configured at install time and is served by the daemon.
- All non-health endpoints require a JWT bearer token obtained from
POST /v1/auth/token. - The API is reachable from any host in the cluster network — not from outside the cluster unless you explicitly expose it.
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 below are reachable on port 30081.
Authentication
Obtain a token using the installer password:
curl -X POST https://<controller-ip>:30081/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 /v1/health
Liveness probe. No authentication required.
curl https://<controller-ip>:30081/v1/health
Response:
{ "status": "ok" }
GET /v1/version
Returns the running daemon version. No authentication required.
curl https://<controller-ip>:30081/v1/version
Response:
{
"version": "3.0.0",
"commit": "abcdef0",
"buildDate": "2026-01-15_12:00:00"
}
POST /v1/auth/token
See Authentication.
GET /v1/nodes
Returns the list of nodes in the cluster.
Request:
curl https://<controller-ip>:30081/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 /v1/create-join-command fail until at least one controller has ready: true, so automation should poll GET /v1/nodes first and only request join commands once a Ready controller is present.
POST /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>:30081/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:
{
"joinSteps": [
{
"description": "Download the join bundle",
"command": "curl -H \"Authorization: Bearer ...\" https://10.0.0.11:30081/v1/join-bundle?roles=controller -o APP_SLUG-join-controller-20260115-120000.tar.gz"
},
{
"description": "Extract the bundle",
"command": "tar xzf APP_SLUG-join-controller-20260115-120000.tar.gz"
},
{
"description": "Run the join command",
"command": "sudo ./APP_SLUG node join"
}
]
}
GET /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>:30081/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 /v1/create-join-command calls it for you with a short-lived, audience-scoped token.
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 /v1/create-join-commandandGET /v1/join-bundle.