Using the API
This API belongs to the console for the clusters I run for you. Your own landscape has no
portal; there you talk to the garden with kubectl or the CLI.
Everything the console does, it does over the same REST API: creating clusters, managing
node pools, issuing kubeconfigs, reading billing. This page is the orientation: the shape of
the API and the conventions that hold across every endpoint. The
API reference then documents each endpoint in full, generated from the live schema.
Base URL and versioning
Section titled “Base URL and versioning”The API lives under the console host, and the stable surface is versioned with a /v1/ prefix:
https://console.paasbox.com/api/v1/Requests and responses are JSON over HTTPS. A new major version would move to /v2/ rather
than change what /v1/ does.
Authentication
Section titled “Authentication”Two ways in, depending on who is calling:
- Session: the console authenticates you with a browser session. That is the interactive path; you do nothing special.
- API key: for scripts, CI, and anything non-interactive. Create a key in the console (see
Teams & API keys), then send it in the
Authorizationheader.
Keys look like pbx_live_…, are shown once at creation, belong to a team member, and can be
revoked at any time. Send it on every request:
curl -s -H "Authorization: Token $PAASBOX_API_KEY" \ https://console.paasbox.com/api/v1/teams/<team>/clusters/Keep the key as the durable secret and use it to fetch a fresh kubeconfig per run rather than storing long-lived cluster credentials.
Everything is scoped to a team
Section titled “Everything is scoped to a team”Resources belong to a team, not to a person, so the paths that own resources carry the team slug:
/api/v1/teams/{team_slug}/clusters//api/v1/teams/{team_slug}/clusters/{cluster}/node-pools/Your key acts within the teams its owner is a member of. The Teams & API keys page covers roles and what a key is allowed to do; the activity log attributes every key-driven change back to a person.
Pagination
Section titled “Pagination”List endpoints are paginated 100 items at a time. Ask for a page with ?page=:
curl -s -H "Authorization: Token $PAASBOX_API_KEY" \ "https://console.paasbox.com/api/v1/teams/<team>/clusters/?page=2"The response wraps the items in a small envelope: count is the total, next and previous
are page URLs (or null at the ends), and results holds the items:
{ "count": 137, "next": "https://console.paasbox.com/api/v1/teams/acme/clusters/?page=3", "previous": "https://console.paasbox.com/api/v1/teams/acme/clusters/?page=1", "results": [ /* … */ ]}Status codes and errors
Section titled “Status codes and errors”Standard HTTP semantics apply. The ones you will meet:
| Code | Meaning |
|---|---|
200 / 201 / 204 | Success: read, created, or deleted (no body). |
202 | Accepted: the work runs asynchronously (e.g. a cluster starting); poll the resource for status. |
401 / 403 | Not authenticated / not allowed. Check the key and the caller’s role in the team. |
402 | A billing or spend limit is blocking the action. Resolve it in the console, then retry. |
404 | No such resource, or it is outside the teams your key can reach. |
409 | Conflict with the current state (e.g. a name already in use). |
412 / 422 | The request was understood but rejected: a precondition failed, or a field did not validate. |
Error responses carry a JSON body describing what went wrong; the status code is what decides how to react.
Two surfaces beside /api/v1/
Section titled “Two surfaces beside /api/v1/”The paasbox CLI, the free path where you run the landscape yourself, talks to two smaller surfaces that do not follow the conventions above. Both are documented here by hand: they are plain endpoints outside the framework the schema is generated from, so they do not appear in the API reference.
Signing in from the CLI
Section titled “Signing in from the CLI”Three endpoints under /api/cli/ on the console host implement the OAuth 2.0 Device Authorization
Grant (RFC 8628) — the flow gh auth login and
docker login use. There is no API key here, because the point is that the caller has none yet; the
brakes are the ten-minute expiry, a rate limit per address, and the fact that nothing happens until
a signed-in human approves the code. Error bodies are RFC 8628’s, not the shapes above, and each may
carry an error_description a client is free to ignore.
POST /api/cli/device/start — open a sign-in. No authentication.
{ "scope": "dns", "client": "paasbox-cli/0.1.0" }{ "device_code": "…43 url-safe characters…", "user_code": "FTQK-2X7M", "verification_uri": "https://console.paasbox.com/device", "verification_uri_complete": "https://console.paasbox.com/device?code=FTQK-2X7M", "expires_in": 600, "interval": 5}400 carries invalid_scope or invalid_request; 429 carries slow_down when too many sign-ins
start from one address.
POST /api/cli/device/token — polled at interval until the human decides. No authentication;
the device code is the credential.
{ "device_code": "…" }While it waits, 400 with one of authorization_pending, slow_down, expired_token,
access_denied, or invalid_grant (a device code that is unknown or already spent); 429 with
slow_down for too many polls from one address. Polling faster than interval returns slow_down
and pushes the gate a further interval out, so a client that ignores it keeps getting it.
Once, and only once, the poll returns 200 with the grant:
{ "token": "…", "team": "Acme GmbH", "label": "acme", "endpoint": "https://dns.paasbox.com/v1", "zone": "paasbox.app", "base": "paasbox.app", "scopes": ["dns"]}Any later poll on the same device code gets invalid_grant. The token is created at collection, not
at approval, so it does not exist anywhere in between. endpoint, zone and base come from the
answering portal’s own configuration rather than a literal, so read them from the response rather
than hard-coding them.
GET /api/cli/whoami — what a stored token is, for paasbox auth status. Send the token as a
bearer credential:
curl -s -H "Authorization: Bearer $DNS_TOKEN" \ https://console.paasbox.com/api/cli/whoami{ "team": "Acme GmbH", "label": "acme", "scopes": ["dns"], "created": "2026-09-07T10:12:31+00:00", "endpoint": "https://dns.paasbox.com/v1", "zone": "paasbox.app", "base": "paasbox.app"}401 with invalid_token means the token is unknown or was revoked — the CLI reads that as “signed
out”, not as a transient failure.
The hosted-name API
Section titled “The hosted-name API”The free hosted names under <team>.paasbox.app are written through a
Hetzner-Cloud-compatible zone and rrset API on its own host:
https://dns.paasbox.com/v1It reproduces the subset of the Hetzner Cloud API that Gardener’s DNS extension, the bring-up and
the hcloud CLI actually call, request for request, and adds the one thing Hetzner cannot: a bearer
token scoped to a team. So an unmodified client works against it — point it at the endpoint and
give it the team token instead of a Hetzner token:
export HCLOUD_ENDPOINT=https://dns.paasbox.com/v1export HCLOUD_TOKEN=$DNS_TOKENhcloud zone rrset set-records --record 203.0.113.7 paasbox.app api.garden.acme A| Endpoint | What it does |
|---|---|
GET /v1/zones | list, filtered by ?name=; returns the one zone your token can see |
GET /v1/zones/{zone} | by id or by name |
GET /v1/zones/{zone}/rrsets | your team’s record sets only |
POST /v1/zones/{zone}/rrsets | create |
GET / DELETE /v1/zones/{zone}/rrsets/{name}/{type} | read or remove one |
POST …/rrsets/{name}/{type}/actions/{action} | set_records, add_records, remove_records, change_ttl |
GET /v1/actions?id= and GET /v1/actions/{id} | action polling, the way Hetzner’s Go client does it |
Request and response bodies are Hetzner’s, unchanged, down to the meta.pagination envelope and the
{"error": {"code": …, "message": …}} shape. Where the upstream refuses something, its answer and
its status code are passed through verbatim.
What is different is what the token may do:
- Writes are allowed to your team’s own name and anything under it. The zone apex, every other
team’s names and all of ours are refused with
403 forbidden. - Reads are filtered. A list shows only your team’s record sets; a read of any other name is
404 not_found, not a403, so the zone cannot be enumerated through this door. - Types are limited to
A,AAAA,CNAMEandTXT.NSis refused anywhere. - Actions are filtered to zone actions, so the sequential action ids of a Hetzner project reveal nothing else.
- Quotas (as of 2026-09-07): 200 records and five active tokens per team.
401 unauthorizedwithout a bearer token,502 upstream_unavailableif Hetzner cannot be reached.
Every write is recorded — team, token, name, type, action, time and the status the upstream returned — including the ones that were refused.
The full reference
Section titled “The full reference”The API reference lists every endpoint, parameter, request body, and response shape. It is generated from the running service’s schema, so it tracks the API as it ships. The two CLI surfaces above are outside that schema and are documented on this page instead.