▦
SrvCloudTECHNOLOGIES
You are reading these docs on — your API base URL is .

SrvCloud Panel API — Complete Documentation

Version: 1.0 · Date: 4 November 2026 · Owner: SrvCloud Technologies (srvcloud.tech)

Companion files: srvcloud-panel-api-openapi.yaml (machine-readable spec) ·

srvdaddy-cloud-whmcs-module/API_CONTRACT.md (the WHMCS-facing contract — the WHMCS

module already built and tested against §6 of this document)


1. What this API is

The SrvCloud panel is a multi-tenant VPS control panel. Customers, resellers and

admins use the web panel; WHMCS sells and bills; the Panel API is the single

backend both of them talk to; and the API alone talks to Proxmox VE through

its official API. Nothing else in the system may call Proxmox — no SSH, no UI

scraping, and Proxmox addresses, node names and credentials never reach a browser.

WHMCS ──┐
        ├──►  Panel API  ──►  Proxmox VE API (per node, token auth)
Panel UI ─┘        │
                   └──►  Panel DB (tenants, VMs, jobs, audit, KYC)

{ "success": true, "data": { … } } or

{ "success": false, "error": { "code": "…", "message": "…" } }.

message is written for humans — WHMCS and the panel show it verbatim.

Proxmox VMIDs are integers; WHMCS-sold VMs also carry

external_id = "whmcs-<serviceid>".

changes ship as /api/v2; v1 keeps working for at least one release cycle.


2. Reseller custom domains — everything is domain-compatible

**Rule zero of this API: no response, link, email or page may ever assume the

domain is panel.srvcloud.tech.** The same API binary serves the master domain

and every reseller's white-label domain (panel.yourdomain.com, …). Anything less

leaks the SrvCloud name to a reseller's customers and breaks their brand.

2.1 Tenant resolution

1. Every request's Host header is looked up in the tenant-domain registry

(table tenant_domains: domain → tenant_id, verified flag, SSL state).

2. The resolved tenant scopes everything that follows: which VMs a token may

see, which branding a page renders, which email templates and sender name are

used, which locations/plans are sellable.

3. An unknown or unverified host gets the master tenant only for the public

status page; API calls on an unknown host return 404 unknown_domain.

4. A domain becomes active through the reseller flow already in the panel:

reseller adds the domain → panel shows the CNAME target → DNS is verified →

a Let's Encrypt certificate is issued automatically (HTTP-01) → domain flips

to Connected. Certificates renew automatically; an expiring/failed cert

raises an admin alert (Audit Log + announcement draft).

2.2 What "domain-compatible" means, surface by surface

SurfaceBehaviour on a reseller domain
Panel UISame app, tenant branding (name, logo, colour, support email). No SrvCloud name anywhere.
API base URLhttps://panel.yourdomain.com/api/v1 works identically. WHMCS server records for that reseller point at their domain.
TokensIssued per tenant; a token minted on one domain is valid on any domain of the same tenant, invalid for every other tenant (404, never 403).
Console URLsMinted on the requesting domain (https://panel.yourdomain.com/console?…), short-lived, single-session. The Proxmox host/ticket never appears.
SSO links (WHMCS → panel)One-time URLs on the tenant's domain, scoped to the VM's owner.
Status pagehttps://<any panel domain>/status/ renders that tenant's view: tenant brand, only the locations/services that tenant sells. Master domain shows the full fleet. Incidents are fleet-level facts, wording stays brand-neutral.
API docshttps://<any panel domain>/docs/ renders these docs with the tenant's brand and that domain pre-filled as the base URL in every example.
EmailsSent from the tenant's domain/sender with the tenant's templates (the editable templates in Reseller → Branding); every link inside (reset, console, panel) uses the tenant domain.
CORSAccess-Control-Allow-Origin echoes only verified tenant domains; the master domain is not a wildcard.
Webhooks/callbacksWHMCS return URLs are taken from the tenant's own WHMCS, never hard-coded.

2.3 Hard rules for implementers

a stored master domain.

rendered per-request).

contain the string srvcloud anywhere in HTML, JSON, email or PDF output

except the audit trail visible to the master admin.


3. Authentication

CallerCredentialHeader
WHMCS moduleModule token (sd_live_…), one per tenantAuthorization: Bearer <token> + X-Client-Id, X-Request-Source: whmcs, X-Request-Id
Customer scriptsPersonal access token (sd_pat_…)Authorization: Bearer <token>
Panel UISession cookie (HttpOnly, Secure, SameSite=Lax) after logincookie
Publicnone/api/v1/health (limited), /api/status

only a SHA-256 hash is stored. Regeneration invalidates the old token

immediately. The panel UI follows this rule today (green show-once box).

tenant's VM returns 404 (existence is never leaked with a 403).

answers 2fa_required and the code is sent to POST /auth/login/2fa.

API tokens are not subject to 2FA (they are the automation path) but are

revocable instantly.

the standard error envelope.


4. Jobs — every long operation is a job

Deploys, reinstalls, migrations and restores take minutes, so they are jobs:

1. The mutating call returns 202 with data: { "job_id": "job_…", "vm": {…} }.

2. The panel UI opens the Live Task Terminal and polls

GET /api/v1/jobs/{job_id} every 2 s.

3. A job carries steps[] — plain-language lines translated from the Proxmox

task (UPID) — plus percent, status: running|success|failed, and the raw

upid for admins.

4. On failure the job's last step is a human sentence ("Frankfurt node is out of

storage"), the VM is rolled back (half-built VM deleted, IP released), and

the failure lands in Tasks & Activity and the Audit Log.


5. Data model (core objects)

domains[], discount_pct (0–90 — the reseller's personal discount on admin

plan prices, set per reseller by the admin; there is NO resource-pool

allotment), and their own retail price per plan (their margin is the gap).

rejected`) gates VM management after the first purchase.

cluster/node/VMID, plan, location, template, IPs, label, tags, app

(one-click install), status (running|stopped|suspended|provisioning|error),

bandwidth used/quota.

token (stored encrypted, never returned).

status (live|coming_soon|maintenance).

only. A reseller's cost for a plan = list price × (1 − discount_pct/100);

a template is

VMID = slug (e.g. 9001 = ubuntu-24.04) exactly as registered in the panel.


6. WHMCS-facing endpoints (normative — module already built)

Base path /api/v1/whmcs/servers/{external_id} unless noted. This section is a

summary; API_CONTRACT.md in the module project is the exact contract and wins

in case of any difference.

EndpointPurpose
GET /api/v1/healthTestConnection → { panel_version, clusters_ok }
POST /api/v1/whmcs/serversCreate/deploy (idempotent on external_id; 201 + full vm object incl. IPs)
GET …/{external_id}Status for client area + admin tab (usage %, last task)
POST …/suspend {reason}Graceful stop, then force; data kept
POST …/unsuspendStart
DELETE …/{external_id}Terminate: VM + disk deleted, IP quarantined 48 h
POST …/resize {plan,…}ChangePackage; disk only grows
POST …/passwordReset root password inside the VM
POST …/power {action}start / shutdown / force_stop / restart
POST …/reinstall {template, password}Wipe + fresh template, same VMID/IP/plan
POST …/snapshotsCreate snapshot
POST …/consoleShort-lived noVNC URL on the requesting domain
POST …/ssoOne-time panel login URL on the tenant's domain

**Wallet rule — every create order, whatever its origin (owner's rule,

5 Nov 2026). A create/deploy order for a reseller tenant** — placed in

the panel, arriving from the reseller's WHMCS (POST /api/v1/whmcs/servers),

or sent to the panel API — executes only when that reseller's wallet

covers the plan cost (the reseller's discounted or per-plan custom cost,

§3). The wallet debit and the VM creation are atomic: on success the wallet

is debited and a wallet transaction + audit entry are written; when the

balance is short, the call fails with wallet_insufficient (402) — no VM

is created, no IP is reserved, and nothing is charged. The platform's own

(Direct) tenant is billed by invoice instead and has no wallet gate. Creates made with the master (admin) token — the admin's own API deploys, for any tenant — are likewise exempt from the wallet gate.

Module v2 (auto catalog, addon, premium client area). The module never

asks an admin to type a plan or location by hand again:

EndpointPurpose
GET /api/v1/plansCatalog sync → { plans: [{ slug, name, vcpu, ram_gb, disk_gb, bandwidth_gb, price }] }
GET /api/v1/locationsCatalog sync → { locations: [{ slug, label, status }] } (only live is orderable)
GET /api/v1/templatesCatalog sync → { templates: [{ slug, label, family }] }
GET …/{external_id}/snapshotsSnapshot list for the client area
DELETE …/snapshots/{name} · POST …/snapshots/{name}/rollbackClient-area snapshot management
GET /api/v1/resellersMaster token only → [{ name, email, domain, api_client_id, api_token }] for personalized downloads

The VM object also carries usage (CPU/RAM/disk %, bandwidth used/limit)

and permissions (power / reinstall / snapshot / console / sso) plus

locked_reason when suspended: the WHMCS client area renders exactly the

controls the panel grants that customer — the same rule as the panel's own

UI. The SrvCloud Manager addon syncs the catalog into WHMCS

configurable options (with INR prices) and can create one product per

plan. Personalized module downloads (panel WHMCS screen, or the

addon's Downloads tab) bake a tenant's host + API token into a one-time

Auto-Setup addon inside the ZIP; activating it in WHMCS creates the

server record with the key already applied.

Password rule (panel-wide, also §7): at deploy/reinstall the root password is

one of Random (generated, shown after completion + emailed), Custom

(min 8 chars, validated before the job starts), or Preset (the customer's

saved preset, stored encrypted, never displayed).


7. Panel-facing endpoints (the web UI's API)

7.1 Auth & account

POST /auth/login · POST /auth/login/2fa · POST /auth/logout ·

GET /auth/sessions · DELETE /auth/sessions/{id} ·

POST /account/2fa/setup → returns the TOTP secret once ·

POST /account/2fa/enable {code} · POST /account/2fa/disable {code} ·

GET/POST /account/api-tokens (create returns the full token once) ·

DELETE /account/api-tokens/{id} · PUT /account/profile

7.2 Servers (customer + admin views of the same resources)

password_mode: random|custom|preset, password?, ssh_key_id?, app?,

owner_id? (admin/reseller) } → 202` + job. Placement picks a node in the

location by capacity; if no node there can fit the plan, the job fails

with capacity_unavailable. Resellers are never blocked by a pool —

their cost is simply computed from their discount_pct.

Proxmox vncproxy, holds the ticket server-side, and proxies the WSS tunnel

on the requesting domain; the browser never learns the node address.

(type-to-confirm is a UI rule; the API requires confirm: "<hostname>").

GET /servers/{id}/resize-quote?plan=… { diff_inr, effective }.

POST /servers/{id}/unassign · POST /servers/{id}/suspend ·

DELETE /servers/{id} (terminate) · POST /servers/{id}/migrate

{target_node} → job · bulk: POST /servers/bulk {ids[], action}.

7.2.1 One-click apps — how an app actually gets installed

Proxmox has no "install app" API call, and the panel never pretends otherwise.

Apps ride on cloud-init, which is already mandatory on every template:

1. Deploy arrives with app (Linux templates only — an app sent with a

Windows template is rejected at validation with app_not_supported).

The deploy UI asks app-or-clean-OS first, then offers only the Linux

templates that app supports; the API applies the same per-app support

matrix, so an unsupported app + template pair can never be ordered.

2. After the clone, the API makes sure the app's user-data snippet exists

on the target node's snippet storage — uploaded once per node via

POST /nodes/{n}/storage/{s}/upload (content=snippets), cached by

app + snippet version.

3. PUT /qemu/{id}/config sets cicustom: user=<storage>:snippets/<app>.yaml

together with the normal cloud-init fields (user, password per the

3-way password rule, SSH keys, IP config), and the VM starts.

4. On first boot the guest's cloud-init runs the app's official install

script (WordPress: stack + wp-cli · Docker: get.docker.com · aaPanel /

CyberPanel: the vendor installer · Nextcloud: AIO · Node: NodeSource +

PM2 · LAMP: distro packages). Snippets contain no secrets — app admin

passwords are generated per deploy and delivered like root passwords

(done screen + email).

5. Completion is verified, not assumed: the API polls the QEMU guest

agent (POST /qemu/{id}/agent, guest-exec on cloud-init status) until

cloud-init reports done. Only then does the job's "Installing <app>" step

close. If cloud-init errors or a 15-minute timeout hits, the job ends

app_install_failed with the guest's cloud-init log excerpt in the job

log — the server itself stays up and usable, and the failure is what the

panel and WHMCS show. Never a fake success.

Templates must therefore ship cloud-init + qemu-guest-agent. A template

without the agent can still deploy, but app installs on it are refused up

front (the panel cannot verify what it cannot see). Heavy apps may

alternatively be shipped as pre-built templates — the deploy flow is

identical, cloud-init then only carries credentials.

7.3 Snapshots, backups, network, firewall (per server)

DELETE …/snapshots/{snap}

{enabled, frequency: daily|weekly}

POST …/network/extra-ip (creates the WHMCS-billable order hook)

(the final default-deny rule is locked and cannot be deleted)

7.4 Infrastructure (admin)

The API verifies before saving as Running: it calls the node's

/api2/json/version and /nodes; an unreachable node is stored as

unreachable, never running. (The panel's standalone checker service

already implements this reachability rule today.)

DELETE /nodes/{name} (refused while it hosts VMs unless force after

migration)

GET/POST/PATCH/DELETE /plans · GET/POST/PATCH/DELETE /templates ·

GET/POST/PATCH/DELETE /ip-pools

{ name, subnet, total, node, bridge } — the pool lives on ONE node's

bridge (Proxmox vmbr0, vmbr1, …). Nodes carry bridges[] and a

default_bridge; the Panel API reads the real bridge list from

GET /nodes/{node}/network (type bridge) when a node connects.

Deploy resolution is fixed: location → node (Running, capacity) → the

node's default bridge → the pool bound to that (node, bridge) → next

free IP. The VM is created with net0: …,bridge=<bridge> and

ipconfig0 from that pool — a VM never receives an IP from another

bridge's subnet. An admin may override the bridge at deploy (only

bridges with a bound pool are selectable for public VMs); customers

and resellers never pick a bridge. The VM record stores bridge and

pool, and a pool with live assignments cannot be deleted.

returns the node's live VM list from Proxmox (GET /nodes/{node}/qemu

plus each VM's config) MINUS the VMs the panel already manages on that

node, matched by VMID — so VMs created directly on Proxmox (or before

the panel existed) surface here with their real name, status, vCPU/RAM/

disk and IP (from the config, or the guest agent when it reports one).

POST /vms/import { node, vmid, customer_id } registers one

discovered VM under a customer (and that customer's tenant). Nothing is

moved or copied on the hypervisor: the panel creates its server record

from the live Proxmox config — plan recorded as Custom (imported)

with the VM's actual specs — and from then on the VM is a normal

managed server (power, console, reinstall, snapshots, billing hooks).

Re-scans are idempotent: a known VMID never shows as discovered again,

and importing the same VMID twice returns the existing record.

verdict. Computed from live VM + node rows.

7.5 Business (admin + reseller)

POST /customers/{id}/login-as (audited, time-boxed)

retail prices) +

/resellers/{id}/team (members + roles: Admin/Servers/Billing/Support)

GET /admin/kyc · POST /admin/kyc/{id}/approve ·

POST /admin/kyc/{id}/reject {reason} — documents are fetched through the

API only by the owning customer and admins; files are never public URLs.

all|<tenant> }`; resellers may post only to their own scope.

· GET/PUT /branding/email-templates/{key} with {variables} preview.

7.6 Status & docs (public, every domain)

services, reachability, response ms, incident history.

current domain.


8. How each operation maps to Proxmox

Panel operationProxmox VE API calls
DeployPOST /nodes/{n}/qemu/{template}/clone → PUT …/qemu/{id}/config (cores, memory, cloud-init user/password/SSH keys/IP) → disk resize → POST …/status/start → poll task UPID
Power`POST …/qemu/{id}/status/{startshutdownstopreboot}`
ConsolePOST …/qemu/{id}/vncproxy → API proxies the websocket; ticket stays server-side
Serial consolePOST …/qemu/{id}/termproxy on the VM's serial port (serial0, socket) → API proxies the websocket. Deploys add the serial port to every VM: Linux templates expose it through cloud-init images, Windows templates through cloudbase-init, which listens on that port — it doubles as the automation/recovery channel when networking inside the VM is down
Reinstallstop → delete VM → clone template to the same VMID → cloud-init with the new password rule → start
Resizeconfig change (cores/memory) + disk resize (grow only); one restart if RAM/CPU change requires it
SnapshotPOST …/qemu/{id}/snapshot · rollback · delsnapshot
MigratePOST …/qemu/{id}/migrate {target} (live, shared/named storage rules enforced)
MetricsGET …/qemu/{id}/status/current + rrddata
Firewall…/qemu/{id}/firewall/rules CRUD
One-click appUpload snippet (once per node) POST /nodes/{n}/storage/{s}/upload → PUT …/qemu/{id}/config (cicustom) → start → poll …/qemu/{id}/agent (guest-exec cloud-init status)
Node testGET /version + GET /nodes with the node's API token
Suspend (billing)graceful shutdown, force after timeout; a panel-side flag blocks customer power actions

Cloud-init is mandatory on every template: password modes, SSH keys, hostname,

one-click apps (first-boot scripts) and IP configuration all ride on it.


9. Errors

Stable machine codes + human messages. The common ones:

validation_failed (422) · unauthorized (401) · forbidden_scope (404 by

design for cross-tenant) · not_found (404) · capacity_unavailable (409) ·

location_unavailable (409 — maintenance/coming soon) · node_unreachable

(502) · proxmox_error (502, message carries the translated reason) ·

kyc_required (403 — management locked until KYC is verified) ·

job_failed (500, with job_id for the terminal/audit trail) ·

wallet_insufficient (402 — the reseller tenant's wallet balance is below

the plan cost; the order is not executed, nothing is created or charged).

Never a bare HTML 500; never a stack trace.


10. Security requirements

Proxmox user per node purpose (provisioning vs console) with least privilege.

shown in full once in the UI — the same rule the panel already follows.

request id, result). WHMCS calls are logged with their X-Request-Id.

one-time and expire in ≤120 s.


11. Build order (implementation slices)

1. Core + auth + tenant/domain resolution (§2, §3) and the node registry

with real verification (§7.4) — against the local Proxmox test install.

2. Deploy + jobs + Live Task Terminal (§4, §7.2) with the password rule.

3. Server detail set: power, console proxy, reinstall, snapshots, network,

firewall, metrics.

4. WHMCS endpoints (§6) — the module is already built and tested against a

mock of this contract; it must work unchanged.

5. Reseller white-label: domains + SSL automation, branding, scoped

announcements, team roles, email templates.

6. Hardening: capacity planner, migration, bulk actions, monitoring hooks

(the 5-minute fleet monitor already runs as the checker service).

End of document — SrvCloud Panel API v1.0

SrvCloud Panel API v1.0 · Service status · Open the panel