---
title: "Create Kubernetes cluster"
method: POST
path: "/api/v1/k8s/clusters"
tags: ["Kubernetes"]
---

# Create Kubernetes cluster

`POST /api/v1/k8s/clusters`

Create a managed Kubernetes cluster. Provisioning is asynchronous — the response returns immediately with `pending` status; the cluster is usable when `ready` is `true` (typically 8–12 minutes).

API-created clusters follow the account's billing mode: pay-as-you-go accounts bill per node-hour; subscription accounts are billed monthly — the API validates the saved payment method, creates the subscription, and charges it automatically (same behavior as VM creation).

Retries are safe with an `Idempotency-Key` header: a repeated request with the same key returns the cluster created by the first request instead of provisioning a second one.

## Headers

- `X-Project-ID` string, uuid, required
- `Idempotency-Key` string

## Request body

- CreateK8sClusterRequest
  - `name` string, required — 1–63 lowercase letters, digits or hyphens; unique within the account
  - `k8s_version_id` integer — From [List Kubernetes versions](#tag/Kubernetes/operation/listK8sVersions). Omit for the default version.
  - `node_pools` K8sNodePoolInput[], required — At least one pool with at least 2 nodes in total. Pools may use different plans — each pool's nodes are sized and billed by its own plan.
    - `name` string, required — 1–63 lowercase letters, digits or hyphens; unique within the cluster
    - `node_count` integer, required — Per-pool minimum is 1; the cluster needs at least 2 worker nodes across all pools
    - `plan_id` integer, required — Worker plan from [List Kubernetes node plans](#tag/Kubernetes/operation/listK8sNodePlans). Pools in one cluster may use different plans.
    - `min_nodes` integer — Autoscaler floor
    - `max_nodes` integer — Autoscaler ceiling
    - `autoscale_enabled` boolean
    - `labels` string — JSON object of Kubernetes labels applied to the pool's nodes, e.g. `{"workload":"web"}`
    - `taints` string — JSON array of taints, e.g. `[{"key":"gpu","value":"true","effect":"NoSchedule"}]`
  - `ha_enabled` boolean — 3-master highly available control plane (flat monthly fee)
  - `firewall_enabled` boolean — Attach the platform baseline firewall to the public gateway
  - `firewall_security_group_id` string, uuid — Use one of your security groups instead of the baseline
  - `storage_node_count` integer — Removed August 26, 2026 — every cluster includes the `raff-block` default StorageClass: PersistentVolumeClaims are provisioned as Volumes and billed per GB. Any value other than 0 is rejected.
  - `storage_node_disk_gb` integer — Removed August 26, 2026 together with `storage_node_count`.
  - `traefik_enabled` boolean — Install the Traefik ingress controller
  - `metallb_enabled` boolean — Install MetalLB for `LoadBalancer` services
  - `cluster_cidr` string — Pod network override (private IPv4, /12–/24)
  - `service_cidr` string — Service network override (private IPv4, /12–/24)
  - `region` 'us-east'

## Response `201`

Cluster accepted for provisioning (status `pending`)

- object
  - `success` boolean
  - `cluster` K8sCluster
    - `id` string, uuid, required — Internal UUID
    - `cluster_id` string, required — Short cluster ID used in URLs and the API endpoint hostname
    - `name` string, required
    - `status` 'pending' | 'deploying' | 'running' | 'warning' | 'failed' | 'deleting' | 'delete_failed' | 'deleted', required — - `pending` / `deploying` — provisioning - `running` — control plane up - `warning` — degraded (see `status_message`) - `failed` — provisioning failed; infrastructure was torn down (see `status_message`) - `deleting` — teardown in progress - `delete_failed` — teardown incomplete; nothing removed — retry the delete
    - `status_message` string — Human-readable detail for warning/failed/delete_failed
    - `ready` boolean, required — `true` when the cluster is running and no node pool is mid-operation — safe to use without interpreting `status`
    - `dns_domain` string
    - `api_endpoint` string
    - `k8s_version` string
    - `k8s_version_id` integer
    - `region` 'us-east'
    - `master_count` integer — 1, or 3 with an HA control plane
    - `worker_count` integer — Total worker nodes across all pools
    - `ha_enabled` boolean
    - `firewall_enabled` boolean
    - `longhorn_enabled` boolean — Legacy clusters only — dedicated storage nodes with Longhorn block storage. Clusters created since August 26, 2026 use the `raff-block` StorageClass (PVCs become Volumes) and report false.
    - `traefik_enabled` boolean
    - `metallb_enabled` boolean
    - `billing_type` 'payg' | 'subscription'
    - `project_id` string, uuid
    - `price_per_hour` number, double — Current hourly price (workers + HA fee; control plane is free, PVC storage bills separately per GB)
    - `price_per_month` number, double
    - `vpc_id` string — Dedicated VPC the cluster runs in
    - `private_vip` string — Cluster gateway IP inside the VPC
    - `node_pools` K8sNodePool[]
      - `id` string, uuid, required
      - `cluster_id` string, uuid
      - `name` string, required
      - `node_count` integer, required — Target node count
      - `actual_node_count` integer — Nodes that currently exist (differs from `node_count` while scaling)
      - `min_nodes` integer
      - `max_nodes` integer
      - `autoscale_enabled` boolean
      - `plan_id` integer
      - `cpu` integer
      - `ram` integer — GB
      - `disk` integer — GB
      - `labels` string
      - `taints` string
      - `status` 'pending' | 'provisioning' | 'running' | 'scaling' | 'failed' | 'deleting' | 'deleted', required — `scaling`/`deleting` are asynchronous — watch `status_message` for the outcome (e.g. a drain blocked by a PodDisruptionBudget)
      - `status_message` string
      - `nodes` K8sNode[]
        - `id` string, uuid
        - `pool_id` string, uuid
        - `cluster_id` string, uuid
        - `name` string
        - `role` 'vnf' | 'master' | 'worker' | 'storage' — `vnf` is the cluster's network gateway
        - `status` 'pending' | 'provisioning' | 'running' | 'not_ready' | 'cordoned' | 'failed' | 'deleting' | 'deleted' — `running` means the kubelet reports Ready; `cordoned` = unschedulable (drain or manual cordon); `not_ready` = joined but failing health checks
        - `cpu` integer
        - `ram` integer — GB
        - `disk` integer — GB
        - `ip_address` string
        - `price_per_hour` number, double
        - `created_at` string, date-time
        - `updated_at` string, date-time
      - `created_at` string, date-time
      - `updated_at` string, date-time
    - `created_at` string, date-time
    - `updated_at` string, date-time

## Other responses

- `400` — Invalid request parameters
- `401` — Authentication required
- `402` — Account balance is insufficient for this operation. Top up the balance and retry.

---

[API](https://skmtc.dev/rafftechnologies/apis/raff-api.md) · [All operations](https://skmtc.dev/rafftechnologies/apis/raff-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/rafftechnologies/raff-api/revisions/2f4df56be77c/schema)
