Skip to content

REST API

The REST API lets an agent or a script do what the portal’s PaaSbox Clusters pages do, with a team API key instead of a browser. This page lists every endpoint with its scope, its body, its answer and its errors, and the rules that apply to all of them.

ItemValue
Base path/api/v1/teams/{team}/k3s/ on the portal’s host. The API keys & agents page shows the full URL for your team.
{team}The team’s slug. It must be the key’s own team; any other answers 404 not_found.
{cluster}The cluster’s slug (its DNS label) or its id (a UUID). A slug finds a cluster that is not deleted; an id also finds a deleted one.
HeaderAuthorization: Bearer <key>. Authorization: Api-Key <key> works too.
Key<prefix>.<secret>, created by a team admin on API keys & agents (Give an agent access). The prefix names the key in the portal and in the audit log.
Browser sessionA signed-in session works on the REST endpoints too, with the person’s own rights and all four scopes.
Conditions on every callThe key is not revoked or expired, its owner is still an active member of the team, and the team has PaaSbox Clusters.
FormatJSON, camelCase fields, times as RFC 3339 strings in UTC.

Every endpoint needs exactly one scope. A key holds the scopes ticked when it was created.

ScopeAllows
k3s:readSee clusters, their health, nodes, snapshots, operations, upgrades and add-ons, and the Hetzner projects and server types to create a cluster on.
k3s:writeTake snapshots, schedule upgrades, and switch or configure add-ons.
k3s:accessAsk for temporary kubeconfigs, sealed to the caller’s own key, and revoke them.
k3s:destructiveCreate, delete, detach and restore clusters, each only with the cluster’s name typed as confirmation.

A key acts with the rights of the person who created it, in that team. Members read; every change needs a team admin; a member gets a view kubeconfig unless the team allows admin. A scope narrows these rights; it never widens them.

A key belongs to one team and reaches every cluster of that team: no scope can be limited to one cluster. A key with k3s:destructive that creates throwaway clusters can also delete production if production is in the same team; keep production in a team the key does not belong to.

Paths are relative to the base path. “Admin” means the key’s owner must be a team admin. The operation id is what the audit log records as the call.

MethodPathScopeAdminAnswerOperation id
GETprojects/k3s:read—200 {items: [{id, name, ref}]}k3s_projects_list
GETprojects/{project}/server-types/k3s:read—200 locations and server typesk3s_projects_server_types
GETclusters/k3s:read—200 a page of clustersk3s_clusters_list
POSTclusters/k3s:destructiveyes202 {cluster}k3s_clusters_create
GETclusters/{cluster}/k3s:read—200 the cluster in fullk3s_clusters_get
DELETEclusters/{cluster}/k3s:destructiveyes202 {cluster}k3s_clusters_delete
GETclusters/{cluster}/status/k3s:read—200 state and healthk3s_clusters_status
GETclusters/{cluster}/nodes/k3s:read—200 {items: [node]}k3s_clusters_nodes
POSTclusters/{cluster}/detach/k3s:destructiveyes200 {cluster}k3s_clusters_detach
GETclusters/{cluster}/snapshots/k3s:read—200 a page of snapshots, newest firstk3s_snapshots_list
POSTclusters/{cluster}/snapshots/k3s:writeyes202 {operation}k3s_snapshots_create
POSTclusters/{cluster}/restore/k3s:destructiveyes202 {operation}k3s_clusters_restore
GETclusters/{cluster}/operations/k3s:read—200 a page of operations, newest firstk3s_operations_list
GETclusters/{cluster}/operations/{operation_id}/k3s:read—200 the operation with its eventsk3s_operations_get
GETclusters/{cluster}/upgrades/k3s:read—200 upgrade informationk3s_upgrades_get
POSTclusters/{cluster}/upgrades/k3s:writeyes202 {operation}k3s_upgrades_schedule
GETclusters/{cluster}/addons/k3s:read—200 the release’s add-onsk3s_addons_list
PATCHclusters/{cluster}/addons/{name}/k3s:writeyes200 the saved add-onk3s_addons_set
POSTclusters/{cluster}/access/k3s:accessfor admin, if the team does not allow members202 {operationId, operation, resultUrl}k3s_access_request
GETclusters/{cluster}/access/{operation_id}/result/k3s:access—202, then 200 oncek3s_access_result
POSTclusters/{cluster}/access/revoke/k3s:accessfor anyone but yourself202 {operation}k3s_access_revoke

A 202 means the portal queued the work. Changes to a cluster run as operations that pbx-agent picks up at its next sync, about every 30 seconds: poll the operation until its state is succeeded or failed. A new cluster is polled with GET clusters/{cluster}/ until its state is ready.

The create page’s form as JSON, checked by that same form. Before the form, the portal checks in this order: a connected Hetzner project, a release offered on the stable channel, the team’s cluster limit, the accepted terms, and billing. A team’s first cluster starts its subscription and is created in the portal, not through the API (402 payment_method_required); so is the first cluster after the team’s subscription has ended. The form is checked against the release on the stable channel, also for channel: early.

FieldTypeRequiredDefault and limits
namestringyes≤ 100 characters
confirmstringyesthe new cluster’s name, typed again
projectintegeryesthe Hetzner project’s id, from GET projects/
slugstring—the DNS label, ≤ 32 characters; made from the name
locationstring—from GET projects/{project}/server-types/
serverTypestring—from the same list
adoptServerobject—instead of a new server: id and confirmName (the server’s name, typed: it is rebuilt), keepVolumes and detachFirewalls (both false)
channelstring—stable (default) or early
topologystring—single
maintenanceobject—days (default saturday, sunday), start 02:00, end 05:00, timezone Europe/Berlin
allowedApiRangeslist of CIDRs—who may reach port 6443; default 0.0.0.0/0 and ::/0
backupobject—holder portal (default) or customer; s3 with endpoint (host, no scheme), bucket, folder, region, accessKey, secretKey; scheduleCron (default 0 */6 * * *); keepCycles 1–500 (default 28)
hcloudTokenobject—mode separate (default) or copy; token for separate
platformobject—profile, one the create form offers for the PaaSbox Platform add-on; observability (default false). Since paasbox-platform 1.0.2 (release 2026.10.2) the platform needs an ACME e-mail (acmeEmail). The portal’s create page asks for it; the API has no field for it, so a create with platform is refused with 422 validation_failed. Create the cluster without platform and switch the platform on with PATCH …/addons/paasbox-platform/
EndpointBody or queryRules
GET projects/{project}/server-types/query locationDefault fsn1, else the project’s first location. Each server type: name, arch, cores, memoryGB, diskGB, cpuType, monthlyNetEUR (Hetzner’s monthly net price). Hetzner’s answer is kept for 10 minutes.
GET clusters/, …/snapshots/, …/operations/query page (from 1), page_size (≤ 100, default 25)The answer is {items, count, next, previous}.
GET …/operations/query state, typeOne of the values below; anything else is 422.
DELETE clusters/{cluster}/confirm, finalSnapshot (default true), as a body or as query parametersThe cluster stops counting for billing at once (Costs and billing); the servers, network and firewall go in the background, after a final snapshot unless finalSnapshot is false.
POST …/detach/confirmThe cluster keeps running; the portal stops managing and billing it.
POST …/restore/snapshot (the name in the snapshot list), confirmThe cluster must be ready or failed. Its API stops while the restore runs, and everything written since the snapshot is lost.
POST …/snapshots/noneThe cluster must be running.
POST …/upgrades/when: window or now; release (optional)Only a ready cluster with no pending upgrade. release is a guard: refused when another release is on offer. window waits for the next maintenance window In progress; now runs at the next sync and reboots the node.
PATCH …/addons/{name}/enabled, options, also (all optional)Only what is named changes. options are merged over the stored ones, null unsets one, secret options are kept when not sent. also names other add-ons this change may switch in the same save: one this add-on requires goes on; one it conflicts with, or one that requires it when it goes off, goes off. The add-ons page’s checks apply: its options schema, requires, conflicts, and memory against the server. A baseline add-on cannot be switched off. The PaaSbox Platform with Flux: {"enabled": true, "options": {"profile": "saas-http01", "acmeEmail": "you@example.com"}, "also": ["flux"]}.
POST …/access/publicKey, role (view default, or admin), lifetimeSeconds (600–86400, default 3600)publicKey is base64 of the 65-byte uncompressed point of an ECDH P-256 key you made for this request. The cluster must be ready, upgrading or restoring.
GET …/access/{operation_id}/result/none202 {operationId, state, ready: false, retryAfterSeconds: 5} while pbx-agent works; 200 {operationId, state, ready: true, sealed, aad, expiresAt} exactly once; 410 gone after it was taken or 5 minutes passed; 409 conflict when the request failed. Only the key, or the session, that asked can take it.
POST …/access/revoke/user: me (default), all, or a member’s id or e-mailDeletes the ServiceAccounts behind the kubeconfigs at the next sync.

The sealed kubeconfig is {alg, epk, nonce, ciphertext} with alg ECDH-P256+HKDF-SHA256+A256GCM. To open it: ECDH of your private key with epk, HKDF-SHA256 with 32 zero bytes of salt and the info pbx-access-v1 to 32 bytes, then AES-256-GCM with nonce and the operation id (aad, UTF-8) as associated data. The portal’s helper paasbox_kubeconfig.py does this (Give an agent access).

ObjectFields
clusterid, name, slug, state, location, serverType, arch, topology, project {id, name}, channel, autoPatchUpgrades, release, desiredRelease, apiEndpoint, publicIPv4, allowedApiRanges, unreachable, lastSeenAt, createdAt
cluster in full (GET clusters/{cluster}/)the cluster, plus status, maintenance (with the next window), nodes, lastSnapshot, backup {holder, schedule, keep, target}, upgrade
statusstate, step, failure {during, step, message}, unreachable, unreachableSince, lastSeenAt, apiHealthy, generation, observedGeneration, certificatesExpireAt, flags ({level, text}, worst first), activeOperation, waitingOperations
nodename, role, state, ready, leader, unschedulable, k3sVersion, release, arch, publicIPv4, diskFreeBytes, lastSeenAt
snapshotname, location (s3 or local, one row per copy), kind, takenAt, sizeBytes, node
operationid, type, state, params, requestedBy, windowBound, notBefore, notAfter, createdAt, acceptedAt, finishedAt, step, failedStep, message, result; with events (seq, at, state, step, message, log, result) on GET …/operations/{operation_id}/
upgrade informationcurrent and available ({version, k3sVersion, status, paused, notes}, available with patch), pending, channel, autoPatchUpgrades, canSchedule, nextWindow
add-onname, title, kind, version, enabled, editable, options, secretOptionsSet, optionsSchema, notEditableOptions, requires, conflicts, memoryMiB, minServerMemoryMiB, fits, memoryByOptions, reported
saved add-onaddon, switched ([{addon, enabled}]), generation, note

No answer carries your Hetzner token, the bucket’s keys or the cluster’s tokens; backup.target is s3://<endpoint>/<bucket>/<folder>.

Value setValues
Cluster statepending, provisioning, ready, upgrading, restoring, deleting, deleted, detached, failed
Operation typesnapshot.save, cluster.restore, cluster.upgrade, server.rejoin, node.drain, node.forget, agent.rotate_key, access.issue, access.revoke, diag.collect, s3.verify
Operation statequeued, delivered, accepted, running, succeeded, failed, unsupported, expired, cancelled

Create, delete, detach and restore need k3s:destructive, a team admin as the key’s owner, and confirm:

  • for delete, detach and restore, the cluster’s name (not its slug), exactly;
  • for create, the new cluster’s name, typed again.

A confirm that is missing fails the body check, 422 validation_failed with field: "confirm"; any other value than the name is 422 confirmation_required. The portal checks the name, not that a person agreed: an agent that knows the name can send it. Whether your agent asks you first is up to its client.

Every error is an RFC 7807 problem document (application/problem+json): type, title, status, detail, code, instance, operationId, and field when one input is at fault. Some add members: requiredScope (insufficient_scope), errors by field (validation_failed), canSwitchWith (add-ons). Switch on code.

StatuscodeWhen
400invalid_requestA malformed request.
401unauthenticatedNo key, or a key that is unknown, revoked or expired; or its owner is no longer an active member.
402payment_method_requiredCreate: the team’s first cluster, which starts the subscription in the portal.
402account_past_dueCreate: the team’s account is past due.
403insufficient_scopeThe key lacks the scope (requiredScope names it), or carries no k3s scope at all.
403admin_requiredThe change needs a team admin, or a member asked for an admin kubeconfig the team does not allow.
403not_enabledPaaSbox Clusters is not enabled for the team.
403terms_not_acceptedCreate: a team admin has not accepted the terms in the portal.
404not_foundNo such team, cluster, snapshot, operation, add-on, project or member, or not yours. Another team’s cluster answers the same.
409conflictThe cluster’s state does not allow it, an operation is already queued, no release is on offer, or Hetzner refused the project’s token.
409quota_exceededCreate: the team’s cluster limit (10 at once, more on request).
410goneThe sealed kubeconfig was already taken or is older than 5 minutes.
422validation_failedA field is wrong; errors lists every field by its API name.
422confirmation_requiredconfirm is missing or not the name.
429rate_limitedToo many calls; Retry-After gives the seconds to wait.
500internal_errorA failure in the portal.
503reconciler_unavailableThe Hetzner API answered with an error.
LimitDefault
Calls per minute, per key (per person for a browser session)120
Changing calls (POST, PATCH, DELETE) per minute, within those20
Windowfixed, one minute
Shared withthe MCP tools of the same key

Every changing call, and every request for and hand-out of a kubeconfig, is recorded with the key, the person, the operation id, the cluster, the arguments without secrets, the outcome and its error code. Refusals for a missing scope, a role, the cluster’s state or a wrong confirmation are recorded too. Not recorded: reads, polls for a kubeconfig that is not ready yet, and calls refused before their check, for an unknown key, the rate limit or a malformed body. The team’s admins read the record on API keys & agents (Give an agent access).

The portal serves the OpenAPI document at /api/schema/, with Swagger UI at /api/schema/swagger-ui/ and ReDoc at /api/schema/redoc/. The endpoints on this page carry tags that start with “PaaSbox Clusters”; the key scheme is TeamKey.

Terminal window
curl -H "Authorization: Bearer $PAASBOX_API_KEY" \
"$PAASBOX_PORTAL/api/v1/teams/acme/k3s/clusters/upcheck-prod/status/"

A delete refused because confirm was not the cluster’s name:

{
"type": "https://app.paasbox.com/errors/confirmation_required",
"title": "Typed confirmation required",
"status": 422,
"detail": "deleting a cluster needs confirm set to the cluster's name, exactly: 'upcheck-prod'",
"code": "confirmation_required",
"instance": "/api/v1/teams/acme/k3s/clusters/upcheck-prod/",
"operationId": null,
"field": "confirm"
}

MCP tools has the same operations as tools for an agent, and Guardrails for agents what these rules stop and what they do not.