Skip to content

REST API

There is exactly one API. The console uses it, the MCP server uses it, and so do you. There is no privileged internal path and therefore no capability gap to discover: if the panel can do something, an API call can, and the reverse.

https://<your-installation>/api/v1

The version is in the path. Within a version, fields are added but not removed or repurposed.

Identifiers are prefixed and opaque. srv_…, lb_…, cert_…, s3s_…, job_…. Never construct one; always use what the API returned.

Reads are immediate, writes are jobs. A GET answers from the control plane database. A POST, PATCH or DELETE that touches infrastructure validates, writes intent and a job, and returns 202 Accepted with both the resource and the job. See Asynchronous operations.

Everything is scoped to your organisation. A resource belonging to someone else returns 404, not 403. Its existence is not disclosed.

Errors are typed.

StatusMeaning
422The request body is wrong. The response lists the offending fields
409Well-formed, but a precondition failed. The message names it
404Does not exist for you
503An upstream (hypervisor, DNS provider) is temporarily unavailable. Retry with backoff

The 409 case deserves attention because it is the one people retry blindly. It means the world is not ready — a port is taken, a subnet has no egress, a resource is being deleted. Retrying the same request produces the same conflict.

PathProduct
/serversServers, power, resize, console, password reset
/ssh-keysPublic keys for server creation
/networksPrivate networks and subnets
/ip-pools, /public-ipsPublic IPs — pool endpoints are operator-only
/firewallsFirewalls and attachments
/gatewaysGateway
/load-balancersLoad balancers and listeners
/certificatesTLS certificates
/k8s/clustersKubernetes, kubeconfig, console, verify
/s3/storesObject storage, buckets, keys
/domainsDomains and DNS
/catalogImages, plans, regions
/jobsJob status
/usageUsage events

The specification is the contract, hand-written and kept current — the TypeScript client types are generated from it, which means a drifting spec breaks the build rather than silently misleading you.

Find it at docs/api/openapi.yaml in the source repository. Feed it to your generator of choice.

Terminal window
Z9=https://panel.example.com/api/v1
AUTH="Authorization: Bearer $Z9_TOKEN"
# What can I create?
curl -s -H "$AUTH" $Z9/catalog | jq '.images[].slug, .packages[].slug'
# Create a server — 202
JOB=$(curl -s -X POST -H "$AUTH" -H 'Content-Type: application/json' \
-d '{"name":"web-1","region":"ist1","image":"ubuntu-24.04","package":"m"}' \
$Z9/servers | jq -r '.job.id')
# Wait properly
until [ "$(curl -s -H "$AUTH" $Z9/jobs/$JOB | jq -r .status)" \
= "succeeded" ]; do sleep 5; done

A production script must also stop on failed; see Asynchronous operations for the full pattern.

Be reasonable. Poll jobs no more than once every five seconds — a job is talking to a hypervisor and a tight loop achieves nothing but load.