Skip to content

Create a cluster

POST
/api/v1/organizations/{orgID}/clusters
curl --request POST \
--url https://api.aetherplatform.cloud/api/v1/organizations/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/clusters \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "name": "example", "region": "example", "k8s_version": "example", "plan": "starter", "public_endpoint": false }'

Creates a cluster and begins asynchronous provisioning. Returns 202 with the cluster record in provisioning status.

orgID
required
string format: uuid

Organization UUID.

Media typeapplication/json
object
name
required

The cluster name: lowercase letters, digits and -, starting and ending with a letter or digit, at most 40 characters, unique within the organization. The length of the organization slug plus the length of the name may not exceed 22 characters, so that the Kubernetes object names composed from them fit; otherwise 422 validation_error naming name.

string
region
required
string
k8s_version
required

A supported Kubernetes version as MAJOR.MINOR (e.g. “1.33”) or full MAJOR.MINOR.PATCH. Bare minors resolve to the latest available patch; a full version is accepted only when its minor is offered by GET /api/v1/k8s-versions and its patch is one this platform has resolved (at or below that minor’s current patch) — otherwise 422 validation_error.

string
plan

The package to bill this cluster from. The catalogue currently offers dev, starter, growth and scale, but the server validates the slug against its own catalogue rather than a fixed list, so treat those four as the values available today and not as the permitted set. Optional: omitted — or sent as an empty string, which is accepted and means the same thing — bills the cluster per resource (“custom”), with no package fee and no allowance. A slug that is unknown or retired is rejected at create time with 422 validation_error carrying plan/unavailable in details. Chosen at create time and immutable afterwards: no update field changes it, so moving between packages, or between a package and custom, means recreating the cluster.

string
public_endpoint

The public apiserver endpoint: the cluster’s control-plane proxy also binds one IPv4 from the region’s public pool and the apiserver is published at ...aetherplatform.cloud. Send true — the public API endpoint is mandatory on every cluster, billed at the published monthly rate for public_ip (€4 at the time of writing; the current rate is shown in the portal and, once published, on the pricing page) as a flat month with no proration, locked the first time the billing sampler observes the address — one charge per cluster per month, on top of the cluster’s other resources, and no package fee includes it. The schema default stays false, but omitting the field or sending false is an implementation gap (tracked internally as I-14), not a supported alternative: the API still accepts it and creates a cluster with no public address, no endpoint charge and no supported access path. Such a cluster’s apiserver answers only on its private network — its own workers and the platform — and in production there is no supported way to reach it from a laptop or a CI pipeline; the kubectl proxy through the platform API (proxy-kubeconfig, not exposed in this reference) is a preview that is switched off in production and, where enabled, reaches private and public clusters alike. Private-network connectivity, which would make the public endpoint optional, is a future capability. Immutable after create; a 409 public_ip_pool_exhausted means the region has no free address.

boolean

Cluster accepted for provisioning.

Media typeapplication/json
object
id
required
string format: uuid
org_id
required
string format: uuid
name
required
string
region
required
string
k8s_version
required
string
status
required
string
Allowed values: provisioning running upgrading degraded deleting failed
status_message
string | null
endpoint

The apiserver URL aether-operator reports for the cluster: the private control-plane proxy address, or the public hostname when public_endpoint is true (which it is on every supported cluster — a cluster without the mandatory public endpoint is an implementation gap, tracked internally as I-14, see public_endpoint). Observed, not derived — null (and omitted from the response) until the cluster has been provisioned far enough for the operator to report one.

string | null
observed_kubernetes_version

The version aether-operator reports once a rollout settles.

string | null
capi_cluster_name

Read-only. The Cluster API object name for this cluster, which is also the name of its TenantCluster and the kubeconfig context name. For clusters created after the random suffix was introduced it is <organization slug>-<name>-<6 lowercase alphanumerics>; older clusters keep <organization slug>-<name>. It is stored once at create time and never recomputed, so it does not change if the organization slug later differs.

string | null
keycloak_realm
string | null
harbor_project
string | null
ceph_pool
string | null
lb_subnet
string | null
worker_subnet

The per-cluster worker /24 (read-only; allocated at create time).

string | null
public_endpoint

Whether this cluster’s apiserver is exposed on a public IPv4 and DNS name in addition to the private control-plane proxy path. Every cluster is required to have one: the public API endpoint is mandatory, billed at the published monthly rate for public_ip (€4 at the time of writing; the current rate is shown in the portal and, once published, on the pricing page) as a flat month with no proration, locked the first time the billing sampler observes the address — one line per cluster per month, see SubscriptionLine.kind public_ip. Set at create time and immutable afterwards; sending a different value to updateCluster is rejected with 422 public_endpoint/immutable. A cluster reading false is an implementation gap (tracked internally as I-14), not a supported configuration: the API still accepts public_endpoint: false at create time, and the resulting cluster has no public address, no endpoint charge and no supported access path. Its apiserver answers only on the cluster’s private network — its own workers and the platform — and in production there is no supported way to reach it from a laptop or a CI pipeline; the kubectl proxy through the platform API (proxy-kubeconfig, not exposed in this reference) is a preview that is switched off in production and, where enabled, reaches private and public clusters alike. Private-network connectivity, which would make the public endpoint optional, is a future capability.

boolean
plan_slug

The package this cluster is billed from, as chosen at create time: one of the package slugs in the catalogue (currently dev, starter, growth, scale; a retired slug can still appear on an existing cluster). null (and omitted from the response) means the cluster is billed per resource (“custom”). Immutable — no endpoint changes it.

string | null
created_at
required
string format: date-time
updated_at
required
string format: date-time
Example
{
"status": "provisioning"
}

Missing or invalid credentials.

Media typeapplication/json
object
error
required
object
code
required

A stable, machine-readable error code.

string
message
required
string
details
Array<object>
object
field
required
string
code
required
string
message
required
string
status_url

The platform status page (https://status.aetherplatform.cloud). Present on 5xx answers written by the API’s error writer and on no other; absent, not null, on 4xx.

string format: uri
decline_code

The payment provider’s decline code. Present only on 402 payment_declined; it is its own field and is never folded into message.

string
invoice_id

Present on exactly these errors of the invoice pay and pay/confirm endpoints: 402 payment_declined, 402 payment_method_required, 409 invoice_already_paid, 409 invoice_not_issued, 409 invoice_below_minimum, 409 payment_not_in_flight, and 409 invoice_void when the void invoice was replaced. It is the id of the live invoice, which after a re-issue is the replacement and not the id in the request URL (on 409 invoice_void from pay/confirm it is the replacement’s id). Absent on every other error — 400 (including discount_code_invalid), 403, 404, the discount_* 409s, 409 invoice_void with no replacement (nothing live to name), every 5xx and 503 code — and never null.

string format: uuid
Examplegenerated
{
"error": {
"code": "example",
"message": "example",
"details": [
{
"field": "example",
"code": "example",
"message": "example"
}
],
"status_url": "https://example.com",
"decline_code": "example",
"invoice_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0"
}
}

Refused by the organization role policy (see Authorization in the API description). error.code is forbidden when the caller is not a member of the organization (or the API token belongs to another organization), insufficient_role when the caller’s organization role is below the operation’s minimum role, and token_not_permitted when the request was authenticated with an API token on an operation API tokens may not use (the API Tokens operations; the handler-gated operations named in the API description: deleting the organization, changing a member’s role or payment_mode, paying an invoice including its confirm step, and discounts; and for a cluster-pinned machine token every operation except the loadbalancer-ips and loadbalancer-vms operations). email_unverified when a portal user’s e-mail address is not verified (see Authentication).

Media typeapplication/json
object
error
required
object
code
required

A stable, machine-readable error code.

string
message
required
string
details
Array<object>
object
field
required
string
code
required
string
message
required
string
status_url

The platform status page (https://status.aetherplatform.cloud). Present on 5xx answers written by the API’s error writer and on no other; absent, not null, on 4xx.

string format: uri
decline_code

The payment provider’s decline code. Present only on 402 payment_declined; it is its own field and is never folded into message.

string
invoice_id

Present on exactly these errors of the invoice pay and pay/confirm endpoints: 402 payment_declined, 402 payment_method_required, 409 invoice_already_paid, 409 invoice_not_issued, 409 invoice_below_minimum, 409 payment_not_in_flight, and 409 invoice_void when the void invoice was replaced. It is the id of the live invoice, which after a re-issue is the replacement and not the id in the request URL (on 409 invoice_void from pay/confirm it is the replacement’s id). Absent on every other error — 400 (including discount_code_invalid), 403, 404, the discount_* 409s, 409 invoice_void with no replacement (nothing live to name), every 5xx and 503 code — and never null.

string format: uuid
Examplegenerated
{
"error": {
"code": "example",
"message": "example",
"details": [
{
"field": "example",
"code": "example",
"message": "example"
}
],
"status_url": "https://example.com",
"decline_code": "example",
"invoice_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0"
}
}

A cluster with this name already exists in the organization, or the composed internal (Cluster API) name already exists; in the latter case error.code is conflict and the request is rolled back: the cluster row is created and removed within the request, so cluster.created and cluster.deleted events are emitted and a public IP may be allocated and freed; retrying the same request is safe. When no unique internal name could be allocated (every random suffix drawn collided) error.code is conflict with the message “could not allocate a unique internal cluster name; retry the request”, nothing is persisted, and a retry is safe. Other 409 codes, such as public_ip_pool_exhausted, are possible.

Media typeapplication/json
object
error
required
object
code
required

A stable, machine-readable error code.

string
message
required
string
details
Array<object>
object
field
required
string
code
required
string
message
required
string
status_url

The platform status page (https://status.aetherplatform.cloud). Present on 5xx answers written by the API’s error writer and on no other; absent, not null, on 4xx.

string format: uri
decline_code

The payment provider’s decline code. Present only on 402 payment_declined; it is its own field and is never folded into message.

string
invoice_id

Present on exactly these errors of the invoice pay and pay/confirm endpoints: 402 payment_declined, 402 payment_method_required, 409 invoice_already_paid, 409 invoice_not_issued, 409 invoice_below_minimum, 409 payment_not_in_flight, and 409 invoice_void when the void invoice was replaced. It is the id of the live invoice, which after a re-issue is the replacement and not the id in the request URL (on 409 invoice_void from pay/confirm it is the replacement’s id). Absent on every other error — 400 (including discount_code_invalid), 403, 404, the discount_* 409s, 409 invoice_void with no replacement (nothing live to name), every 5xx and 503 code — and never null.

string format: uuid
Examplegenerated
{
"error": {
"code": "example",
"message": "example",
"details": [
{
"field": "example",
"code": "example",
"message": "example"
}
],
"status_url": "https://example.com",
"decline_code": "example",
"invoice_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0"
}
}

Request validation failed.

Media typeapplication/json
object
error
required
object
code
required

A stable, machine-readable error code.

string
message
required
string
details
Array<object>
object
field
required
string
code
required
string
message
required
string
status_url

The platform status page (https://status.aetherplatform.cloud). Present on 5xx answers written by the API’s error writer and on no other; absent, not null, on 4xx.

string format: uri
decline_code

The payment provider’s decline code. Present only on 402 payment_declined; it is its own field and is never folded into message.

string
invoice_id

Present on exactly these errors of the invoice pay and pay/confirm endpoints: 402 payment_declined, 402 payment_method_required, 409 invoice_already_paid, 409 invoice_not_issued, 409 invoice_below_minimum, 409 payment_not_in_flight, and 409 invoice_void when the void invoice was replaced. It is the id of the live invoice, which after a re-issue is the replacement and not the id in the request URL (on 409 invoice_void from pay/confirm it is the replacement’s id). Absent on every other error — 400 (including discount_code_invalid), 403, 404, the discount_* 409s, 409 invoice_void with no replacement (nothing live to name), every 5xx and 503 code — and never null.

string format: uuid
Examplegenerated
{
"error": {
"code": "example",
"message": "example",
"details": [
{
"field": "example",
"code": "example",
"message": "example"
}
],
"status_url": "https://example.com",
"decline_code": "example",
"invoice_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0"
}
}

A required backend dependency is unavailable.

Media typeapplication/json
object
error
required
object
code
required

A stable, machine-readable error code.

string
message
required
string
details
Array<object>
object
field
required
string
code
required
string
message
required
string
status_url

The platform status page (https://status.aetherplatform.cloud). Present on 5xx answers written by the API’s error writer and on no other; absent, not null, on 4xx.

string format: uri
decline_code

The payment provider’s decline code. Present only on 402 payment_declined; it is its own field and is never folded into message.

string
invoice_id

Present on exactly these errors of the invoice pay and pay/confirm endpoints: 402 payment_declined, 402 payment_method_required, 409 invoice_already_paid, 409 invoice_not_issued, 409 invoice_below_minimum, 409 payment_not_in_flight, and 409 invoice_void when the void invoice was replaced. It is the id of the live invoice, which after a re-issue is the replacement and not the id in the request URL (on 409 invoice_void from pay/confirm it is the replacement’s id). Absent on every other error — 400 (including discount_code_invalid), 403, 404, the discount_* 409s, 409 invoice_void with no replacement (nothing live to name), every 5xx and 503 code — and never null.

string format: uuid
Examplegenerated
{
"error": {
"code": "example",
"message": "example",
"details": [
{
"field": "example",
"code": "example",
"message": "example"
}
],
"status_url": "https://example.com",
"decline_code": "example",
"invoice_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0"
}
}