Skip to content

HTTP API reference

The /api/v1 API manages hostings, backups, certificates and nodes from scripts. It is served by the panel itself. The hctl remote commands in the CLI reference call the same endpoints.

Create a key under API keys in the sidebar (Administration). Tick the capabilities the key may use, optionally set Expires, Allowed addresses and Requests per minute, and click Create key. The panel shows the key once, starting with hyp_. Keep the key off the command line. Arguments of a running program are readable by other local users through /proc, so a key written as a curl argument leaks. Read the key into a shell variable without echo, and hand it to curl on standard input as a config line:

Terminal window
read -rs API_KEY
hyp() { printf 'header = "Authorization: Bearer %s"\n' "$API_KEY" | curl -sS -K - "$@"; }

printf is a shell builtin, so the key never appears in any process’s arguments. Every example below uses hyp in place of curl, and the first call is:

Terminal window
hyp https://panel.example.com/api/v1/me

hyp takes standard input for the key, so it cannot be combined with -d @-. Use -d '<json>' or -d @file. A body that carries a password goes in a file readable only by you, with -d @file, so the password stays off the command line. That applies to admin_password on wp/install and basic_auth_password on vhost. See the example under WordPress.

  • A key can never do more than the person who created it. Only an account with cluster-wide scope can own a key.
  • A header without the hyp_ prefix, or without Bearer , is treated as no key. A missing, expired, revoked or unknown key answers 401.
  • Allowed addresses (empty means any): a request from another address answers 403 with source IP not allowed for this API key. Hyperion reads the client address from the connection, and from X-Real-IP only when the connection comes from the panel’s own nginx.
  • Requests per minute (0 means unlimited): over the limit answers 429 with a Retry-After: 60 header.
  • Session cookies are not accepted on /api/v1; a request without a valid key answers 401.

The OpenAPI document is public, no key needed: GET /api/v1/openapi.json. GET /api/v1/docs shows it in a viewer.

Every error has the same JSON shape:

{ "error": { "code": "forbidden", "message": "API key lacks capability 'hosting_view'" } }
Status code When
400 validation The body or a field is invalid. The message says which.
401 unauthorized missing or invalid API key
403 forbidden The key lacks the capability, may not manage this hosting, or the endpoint needs a cluster-wide key.
404 not_found no such hosting, no such job, or another missing resource.
409 conflict The request conflicts with existing state, for example a hosting for the domain already exists.
429 rate_limited rate limit exceeded for this API key
502 upstream_error The agent or a node did not answer, or answered with an error.
  • :id in a path is a hosting id or its domain, for example 01J0000000000000000000000A or example.com.
  • A slow action answers 202 with { "job_id": "<id>", "status": "accepted" }. Poll GET /api/v1/jobs/<job_id> until state is done, failed or cancelled.
  • Endpoints that change a hosting need the capability in the table below. The key must also be allowed to manage that hosting. Keys are always cluster-wide, so the capability decides.
  • A change made with a key is recorded in the audit log under the actor apikey:<label>.
  • php_version is written v8_1, v8_2, v8_3 or v8_4 in JSON bodies and responses. The CLI accepts 8.3 and converts it.
Method and path Capability Answer
GET /api/v1/me any valid key 200
GET /api/v1/hostings hosting_view 200
POST /api/v1/hostings hosting_create 201
GET /api/v1/hostings/:id hosting_view 200
DELETE /api/v1/hostings/:id hosting_delete 202 job
POST /api/v1/hostings/:id/suspend hosting_suspend 200
POST /api/v1/hostings/:id/resume hosting_suspend 200
PATCH /api/v1/hostings/:id/limits hosting_edit_config 200
PATCH /api/v1/hostings/:id/php hosting_edit_config 200
PATCH /api/v1/hostings/:id/vhost hosting_edit_config 200
PATCH /api/v1/hostings/:id/expiry hosting_edit_config 200
PATCH /api/v1/hostings/:id/quota hosting_edit_config 200
POST /api/v1/hostings/:id/backup backup_run 202 job
GET /api/v1/hostings/:id/backups backup_run 200
POST /api/v1/hostings/:id/restore backup_restore 202 job
POST /api/v1/hostings/:id/cert cert_manage 202 job
POST /api/v1/hostings/:id/wp/install wp_manage 202 job
POST /api/v1/hostings/:id/wp/core-update wp_manage 202 job
POST /api/v1/certs/renew-all cert_manage, cluster-wide key 202 job
GET /api/v1/nodes nodes_view 200
GET /api/v1/jobs/:id cluster-wide key 200

GET /api/v1/me returns the key’s id, label, caps (the capability names the key holds) and scope_all.

Terminal window
hyp https://panel.example.com/api/v1/me

GET /api/v1/hostings returns { "items": [...], "next_cursor": "<id>" | null, "total": N }. Query parameters:

Parameter Meaning
limit Page size, 1 to 200. Default 50.
cursor The next_cursor of the previous page.
state Only hostings in this state, for example active or suspended.
node Only hostings on this node id.
q Case-insensitive part of the domain.

Items are ordered by id, which sorts by creation time. total counts every match, not only the page. Each item has id, domain, state, php_version, created_at, node_id, maintenance_mode, kind, wp_version, cert_not_after, cert_issuer, disk_bytes, last_backup_at and last_backup_ok.

Terminal window
hyp "https://panel.example.com/api/v1/hostings?state=active&limit=100"

GET /api/v1/hostings/:id returns the full detail: the summary fields plus aliases, system_user, root_dir, database, cert, vhost_options and wp_extras.

Terminal window
hyp https://panel.example.com/api/v1/hostings/example.com

POST /api/v1/hostings creates the hosting before it answers. The body is JSON:

Field Meaning
domain Required. A leading www. is removed.
aliases List of extra domains. Default empty.
php_version v8_1 to v8_4.
database mariadb or postgres. Omit for no database.
system_user Put the site under an existing system user. Only a cluster-wide key may set it; otherwise 400 validation.
kind php (default) or reverse_proxy.
proxy_upstream_url Required when kind is reverse_proxy.
node Node id to create the site on. Empty, missing or local means the master.

A domain that already exists on any node answers 409 conflict with a hosting for example.com already exists. The 201 body has id, system_user, root_dir, db (the database name, user and password, shown only in this answer), cert and wp.

Terminal window
hyp -X POST https://panel.example.com/api/v1/hostings \
-H "Content-Type: application/json" \
-d '{"domain":"new.example.com","php_version":"v8_3","database":"mariadb"}'

DELETE /api/v1/hostings/:id runs as a job. With no query parameters it removes the files, the database, the system user and the vhost, unless Trash is on (see Suspend, resume or delete a website). Then the site goes to Trash instead and is removed after the retention time.

Terminal window
hyp -X DELETE "https://panel.example.com/api/v1/hostings/old.example.com?keep_database=true"

Both answer at once. Suspend returns { "id": "...", "state": "suspended" } and resume returns "state": "active".

Terminal window
hyp -X POST https://panel.example.com/api/v1/hostings/example.com/suspend

PATCH /api/v1/hostings/:id/limits replaces the whole limits object, so send every required field. Read the current values from GET /api/v1/hostings/:id first.

Field Type
php_memory_mb, php_max_exec_secs, php_max_children, php_max_requests, db_max_connections Required integers.
over_bw_policy Required. suspend or throttle. Accepted and not enforced.
disk_soft_bytes, disk_hard_bytes, inode_soft, inode_hard, bw_monthly_bytes, throttle_kbps Integer or null for no limit. bw_monthly_bytes and throttle_kbps are accepted and not enforced.
Terminal window
hyp -X PATCH https://panel.example.com/api/v1/hostings/example.com/limits \
-H "Content-Type: application/json" \
-d '{"php_memory_mb":256,"php_max_exec_secs":60,"php_max_children":5,"php_max_requests":1000,"db_max_connections":25,"over_bw_policy":"suspend"}'

PATCH /api/v1/hostings/:id/php with { "version": "v8_3" } answers { "id": "...", "php_version": "v8_3" }. A hosting that is not a PHP site answers 400.

Terminal window
hyp -X PATCH https://panel.example.com/api/v1/hostings/example.com/php \
-H "Content-Type: application/json" \
-d '{"version":"v8_3"}'

PATCH /api/v1/hostings/:id/vhost takes the hosting’s vhost_options object, plus an optional basic_auth_password. Fields you leave out take their type default (false, 0 or an empty string), so start from the vhost_options that GET /api/v1/hostings/:id returned and change what you need. The fields include force_https, hsts_max_age, basic_auth_enabled, basic_auth_user, maintenance_mode, fastcgi_cache_enabled, fastcgi_cache_ttl, redirect_url, redirect_code, redirect_preserve_path, canonical_host, noindex, waf_level, waf_overrides, wp_admin_allowlist, blocked_bots and blocked_countries.

To send basic_auth_password, put the body in a file and use -d @file, as described under Authentication. The example below sends no password, so it stays inline.

Changing custom_nginx_snippet also needs the hosting_edit_nginx_raw capability. Without it the call answers 403 when the snippet differs from the stored one, and an unchanged snippet is accepted.

Terminal window
hyp -X PATCH https://panel.example.com/api/v1/hostings/example.com/vhost \
-H "Content-Type: application/json" \
-d '{"force_https":true,"hsts_max_age":31536000,"maintenance_mode":false}'

PATCH /api/v1/hostings/:id/expiry takes grace_days and warning_offsets_days (a comma list such as "30,7,1"), both required, and optionally expires_at (Unix seconds, or null for no expiry) and owner_email. A past expires_at is moved to the next anniversary of that day and month.

Terminal window
hyp -X PATCH https://panel.example.com/api/v1/hostings/example.com/expiry \
-H "Content-Type: application/json" \
-d '{"expires_at":1798675200,"owner_email":"owner@example.com","grace_days":7,"warning_offsets_days":"30,7,1"}'

PATCH /api/v1/hostings/:id/quota takes the required integers disk_soft_kib, disk_hard_kib, mem_limit_mib, bw_soft_mib and bw_hard_mib, and exceed_action: notify (the default when empty) or suspend. Only the two disk fields and exceed_action take effect. The other three are accepted and not enforced, so send them as 0. It answers with the stored quota.

Terminal window
hyp -X PATCH https://panel.example.com/api/v1/hostings/example.com/quota \
-H "Content-Type: application/json" \
-d '{"disk_soft_kib":1048576,"disk_hard_kib":2097152,"mem_limit_mib":0,"bw_soft_mib":0,"bw_hard_mib":0,"exceed_action":"notify"}'

POST /api/v1/hostings/:id/backup starts a backup and answers 202 with a job id. GET /api/v1/hostings/:id/backups returns { "items": [...] }, newest first, with limit from 1 to 500 (default 50). Each item has id, target, started_at, finished_at, state, archive_path, db_dump_path, bytes_total, error_message, sha256_hex, remote_blob_key, remote_state and remote_error.

Terminal window
hyp -X POST https://panel.example.com/api/v1/hostings/example.com/backup

POST /api/v1/hostings/:id/restore takes archive_path (from a backups item, required) and mode: files_and_db (default), db_only or files_only.

Terminal window
hyp -X POST https://panel.example.com/api/v1/hostings/example.com/restore \
-H "Content-Type: application/json" \
-d '{"archive_path":"<archive_path from the backups list>","mode":"files_only"}'

POST /api/v1/hostings/:id/cert issues a Let’s Encrypt certificate as a job. The body is optional. { "staging": true } uses the Let’s Encrypt staging environment, which browsers do not trust; the default is a production certificate. The DNS check always runs, so a domain that does not point at the node fails the job.

Terminal window
hyp -X POST https://panel.example.com/api/v1/hostings/example.com/cert

POST /api/v1/certs/renew-all renews certificates on the master and on every enrolled node, as a job. It needs a cluster-wide key with cert_manage.

POST /api/v1/hostings/:id/wp/install takes site_url, title, admin_user, admin_email and admin_password, all required, and optionally locale (default en_US), version (default latest) and no_index.

Terminal window
read -rs ADMIN_PW
(umask 077; printf '{"site_url":"https://example.com","title":"Example","admin_user":"admin","admin_email":"owner@example.com","admin_password":"%s"}' "$ADMIN_PW" > wp-install.json)
hyp -X POST https://panel.example.com/api/v1/hostings/example.com/wp/install \
-H "Content-Type: application/json" \
-d @wp-install.json
rm wp-install.json

The printf line does no JSON escaping, so this form needs a password without " or \. The file holds the password, so delete it with rm wp-install.json even when hyp fails.

POST /api/v1/hostings/:id/wp/core-update takes { "version": "6.7.1" }. The version must be one WordPress offers this site. The job takes a full backup first and stops if the backup fails; a version the site is not offered fails the job before anything is touched.

Terminal window
hyp -X POST https://panel.example.com/api/v1/hostings/example.com/wp/core-update \
-H "Content-Type: application/json" \
-d '{"version":"6.7.1"}'

GET /api/v1/nodes returns the enrolled worker nodes: node_id, label, agent_version, public_ip, enrolled_at, last_seen_at, is_drained and drain_reason, among others.

Terminal window
hyp https://panel.example.com/api/v1/nodes

GET /api/v1/jobs/:id returns the job: id, kind, target, state (running, done, failed or cancelled), step_label, progress_pct, substeps, log_tail, error, started_at, updated_at and finished_at. A key that is not cluster-wide answers 403. An unknown id answers 404 no such job.

Terminal window
hyp https://panel.example.com/api/v1/jobs/<job_id>

The names a key can hold are: hosting_view, hosting_create, hosting_delete, hosting_suspend, hosting_edit_config, hosting_edit_nginx_raw, hosting_files, hosting_databases, hosting_cron, hosting_migrate_clone, wp_manage, wp_vuln_view, backup_run, backup_restore, backup_targets, cert_manage, security_manage, monitoring_view, monitoring_manage, nodes_view, nodes_manage, services_view, services_manage, users_manage, roles_manage, settings_manage, profiles_manage, audit_view, email_log_view, panel_import, trash_manage and api_keys_manage. Only the ones in the endpoint table are checked by /api/v1.