Skip to content

API reference

The panel is driven entirely by this HTTP API — the web UI is just a client of it. It listens on 127.0.0.1:5252 and is reached through nginx.

POST /api/login with an email and password sets a session cookie. Send that cookie with every subsequent request.

Terminal window
curl -sk -c jar -b jar https://your-panel/api/login \
-H 'Content-Type: application/json' \
-d '{"email":"you@example.com","password":"your-password"}'
curl -sk -c jar -b jar https://your-panel/api/sites

The cookie is HttpOnly, Secure and SameSite=Strict, and the token is an opaque random value stored in the database, so sessions can be revoked. Sessions last 24 hours.

If the account has 2FA, the first response is 401 with {"totp_required": true}; repeat the request including "totp":"123456". Each code works once.

Every endpoint below is marked with the role it needs.

MarkerRequires
openno authentication
authany signed-in account
manageadmin or operator (operators only for their own sites)
globaladmin or readonly — not operator, as these are server-wide
adminadmin only

An operator sees and touches only assigned sites; requests for others return 404, not 403, so the API does not confirm that a site exists.

  • JSON in, JSON out.
  • Long operations return 202 with a task; poll the resource or GET /api/tasks/{id}.
  • Errors are {"error":"..."} with a meaningful status code.
  • Request bodies are capped at 4 MB and unknown fields are rejected.
MethodPathRolePurpose
GET/api/bootstrapopenwhether initial setup is complete
POST/api/setupopenconsume the one-time token, create the first admin
POST/api/loginopensign in
POST/api/logoutauthsign out and delete the session
GET/api/meauththe current account
GET/healthzopenliveness
MethodPathRolePurpose
POST/api/account/passwordauthchange your own password
POST/api/account/2fa/beginauthstart TOTP enrolment; returns the secret
POST/api/account/2fa/confirmauthconfirm with a code and switch 2FA on
POST/api/account/2fa/disableauthdisable 2FA (requires your password)
GET/api/account/sessionsauthyour active sessions
DELETE/api/account/sessions/{id}authrevoke one
MethodPathRolePurpose
GET/api/usersgloballist accounts
POST/api/usersadmincreate one — role is admin, operator or readonly; an operator needs site_ids
DELETE/api/users/{id}admindelete an account
MethodPathRolePurpose
GET/api/sitesauthlist (scoped to the caller)
POST/api/sitesmanagecreate — returns 202 and a task
GET/api/sites/{id}authone site
DELETE/api/sites/{id}managedelete — returns 202
PUT/api/sites/{id}/configmanageupdate the site configuration
PUT/api/sites/{id}/phpmanagechange PHP version or limits
POST/api/sites/{id}/certificatemanageissue a Let’s Encrypt certificate
POST/api/sites/{id}/purgemanagepurge the page cache
POST/api/sites/{id}/warmmanagecrawl the sitemap to warm the cache
GET/api/sites/{id}/cache-statsauthobject-cache hit rate and memory
POST/api/sites/{id}/migrationmanageimport an existing site from an archive + SQL dump

POST /api/sites/{id}/migration takes {"archive": "...", "sql": "...", "old_domain": "...", "confirm": "<destination domain>"}. archive and sql are paths inside the destination site’s directory — upload them over SFTP first. old_domain drives the search-and-replace through the imported database, and confirm must equal the destination domain or the request is rejected.

Creating a site:

Terminal window
curl -sk -c jar -b jar https://your-panel/api/sites \
-H 'Content-Type: application/json' \
-d '{"domain":"shop.example.com","type":"woocommerce","profile":"commerce",
"admin_email":"you@example.com","admin_user":"you",
"admin_password":"strong-password","object_cache":true}'
MethodPathRolePurpose
GET/api/sites/{id}/deploymentsauthdeployment history with Guard verdicts
POST/api/sites/{id}/deploymentsmanagecreate a release
POST/api/deployments/{id}/promotemanagepromote a specific release
POST/api/sites/{id}/rollbackmanageroll back to the previous release
POST/api/sites/{id}/stagingmanageclone production to staging
GET/api/sites/{id}/staging/tablesmanagewhat a database sync would copy
POST/api/sites/{id}/staging/databasemanagere-sync the staging database
POST/api/sites/{id}/safe-pushmanagemeasure and promote if it holds up
MethodPathRolePurpose
GET/api/sites/{id}/backupsauthlist snapshots
POST/api/sites/{id}/backupsmanagetake one — kind is full, files or database
POST/api/backups/{id}/verifymanagerestore it to scratch and check it
POST/api/backups/{id}/restoremanagerestore, in place or to a target directory
POST/api/backups/testadmincheck the repository is reachable

Jailed to the site root. Paths are resolved and then opened with openat2(RESOLVE_NO_SYMLINKS), so a symlink cannot redirect the operation outside the site.

MethodPathRolePurpose
GET/api/sites/{id}/files?path=authlist a directory
GET/api/sites/{id}/files/read?path=authread a text file (2 MB cap)
POST/api/sites/{id}/files/writemanagewrite a text file
GET/api/sites/{id}/files/download?path=authdownload (16 MB cap; use SFTP above that)
POST/api/sites/{id}/files/uploadmanageupload (16 MB cap)
POST/api/sites/{id}/files/managemanagedelete, rename, mkdir
MethodPathRolePurpose
GET/api/sites/{id}/databaseauthtables, row counts, sizes
POST/api/sites/{id}/database/querymanagerun SQL
POST/api/sites/{id}/database/exportmanagedump to the site’s shared/exports/
POST/api/sites/{id}/database/importmanageimport a .sql from inside the site
POST/api/sites/{id}/database/adminermanagelaunch a time-limited Adminer session

The console is read-only unless you pass "allow_writes": true, and a read-only query must be a single statement — select 1; drop table x is refused. Queries run as a throwaway account granted on that site’s database only, so cross-database access fails at the server.

MethodPathRolePurpose
POST/api/sites/{id}/wp/loginmanageone-click login URL (single-use, 5 minutes)
GET/api/sites/{id}/wp/pluginsauthplugins and themes with available updates
POST/api/sites/{id}/wp/updatemanageupdate core, plugins, themes or all
POST/api/sites/{id}/wp/object-cachemanageenable or disable the object cache
MethodPathRolePurpose
POST/api/sites/{id}/sftpmanageenable SFTP and set the password
GET/api/sites/{id}/ssh-keysauthlist authorised keys
POST/api/sites/{id}/ssh-keysmanageadd a key
DELETE/api/sites/{id}/ssh-keys/{fingerprint}manageremove one
MethodPathRolePurpose
GET/api/sites/{id}/cronauththis site’s jobs
POST/api/sites/{id}/cronmanagecreate one
DELETE/api/cron/{id}managedelete one
POST/api/cron/{id}/runmanagerun it now
MethodPathRolePurpose
GET/api/system/statusglobalCPU, memory, disk headroom, service state
GET/api/system/metricsglobal24 hours of capacity samples
GET/api/system/driftglobalmanaged files that no longer match
POST/api/system/drift/{id}/resolveadmin{"action":"restore"} or {"action":"accept"}
GET/api/servicesglobalnginx, PHP-FPM, MariaDB, Redis state
POST/api/services/{name}/restartadminrestart one
GET/api/logsglobaltail a server log
GET/api/firewallglobalufw rules
POST/api/firewall/ruleadminadd or remove a rule
GET/api/auditglobalaudit log — who did what, from where
GET/api/settingsglobalpanel settings
PUT/api/settingsadminupdate them
POST/api/panel/certificateadmininstall a certificate for the panel itself
POST/api/panel/updateadminself-update, health-checked with automatic rollback

POST /api/panel/update takes {"base_url": "https://…", "version": "v0.1.0"}. base_url is the release root holding the binaries and their .sha256 files; it must be https:// and is required in practice. See Operations.

MethodPathRolePurpose
GET/api/tasksauthrecent tasks
GET/api/tasks/{id}authone task with its log
GET/api/eventsauthserver-sent events stream for live updates
MethodPathRolePurpose
POST/api/connector/purgesite tokencalled by the WordPress mu-plugin

Authenticated with a per-site bearer token, not a session. A site’s token can only purge URLs whose host matches that site’s own domains.