Home / Documentation

Provisioning API (v1)

Create and manage hosting accounts from a billing system.

The provisioning API lets a billing system (WHMCS, Blesta, Paymenter, a home-grown portal) create and manage NuvoPanel hosting accounts. It is a small, versioned, server-to-server surface under

https://<panel-host>:<port>/api/provisioning/v1

A ready-made WHMCS module ships with NuvoPanel: on an installed server it is in /opt/serverpanel/src/integrations/whmcs/. Any other billing system can call the API directly.

Authentication

Every request carries a provisioning token:

Authorization: Bearer ap_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

A provisioning token is an ordinary NuvoPanel API token created with the scope Provisioning (billing) on the API Tokens page (administrators) or the Billing API page (resellers). It is shown once; only its SHA-256 is stored.

OwnerPort to useReaches
Administrator2087 (admin tier)every hosting account on the server
Reseller2083 (client tier)only the reseller's own accounts; reseller quotas apply

The scopes are disjoint, in both directions:

  • a provisioning token can call only /api/provisioning/v1/* — any other panel route answers 403;
  • no other credential can call the provisioning API — panel API tokens get 403 TOKEN_SCOPE, browser sessions and "Login As" sessions get 401 AUTH_REQUIRED.

Everything else about tokens applies unchanged: expiry, revocation, the optional IP allowlist (strongly recommended: the billing server's address), and automatic revocation when the owner's password is changed or reset, the owner is suspended or deactivated, or the owner's role changes. An admin token is refused on 2083 and a reseller token on 2087, exactly like interactive logins.

Firewall. Port 2087 is restricted to an IP allowlist on the panel host. Add the billing server's public IP to the panel firewall (Security › Firewall) before testing the connection. Port 2083 is public.

Conventions

  • Request and response bodies are JSON (Content-Type: application/json).
  • :username is the panel username of the hosting account (the same name WHMCS stores as the service username).
  • Plans may be given by id (10, "10") or by exact name (case-insensitive). Use GET /plans to discover them.
  • Sizes are in MB. null limits mean unlimited / unmetered.
  • Unknown fields in POST /accounts are rejected (VALIDATION_FAILED).

Errors

Every error has the same shape:

{ "statusCode": 404, "error": "ACCOUNT_NOT_FOUND", "message": "Account not found" }

Branch on error; message is a human sentence suitable for showing to staff. Codes are stable within v1.

HTTPerrorMeaning
401AUTH_REQUIREDNo ap_ bearer token (e.g. a browser session was sent)
401AUTH_FAILEDToken unknown, revoked, expired, wrong IP, wrong port/tier, owner suspended
403TOKEN_SCOPEThe token is not a provisioning token
403FORBIDDENNot allowed for this owner
403QUOTA_EXCEEDEDReseller account / disk / bandwidth allocation limit
403SUSPENDED_BY_ADMINA reseller tried to lift an administrator's suspension
400VALIDATION_FAILEDBad or missing input
400CONFIRM_REQUIREDTerminate without {"confirm": true}
400NOT_SUPPORTEDe.g. mail-only accounts with a reseller token
404ACCOUNT_NOT_FOUNDNo such account owned by this token (never 403, so names cannot be probed)
404PLAN_NOT_FOUNDPlan unknown or not assignable by this token
409DOMAIN_EXISTSDomain belongs to another account (or is an addon/subdomain)
409USERNAME_TAKENRequested username is in use
409ACCOUNT_SUSPENDEDSSO into a suspended/disabled account
409CONFLICTOther conflicts (e.g. two plans with the same name)
422OPERATION_FAILEDThe panel started but could not finish (e.g. termination kept the account)
429RATE_LIMITEDToo many requests
500INTERNAL_ERRORUnexpected failure; details are in the panel log, not the response

Rate limits

Per client IP, in addition to nginx's API zone:

  • all endpoints: 10 requests/second, 120/minute;
  • POST /accounts, POST …/password: 2/second, 30/minute;
  • POST …/terminate: 2/second, 20/minute;
  • POST …/sso: 5/second, 60/minute.

Exceeding a limit answers 429 RATE_LIMITED; retry after a short pause.

Idempotency

Billing systems retry after timeouts, so the API is safe to repeat:

CallOn repeat
POST /accountsSame domain, same owner (and same username if given) → 200 with "created": false and the existing account. Concurrent duplicates are serialised.
POST …/suspendAlready suspended → 200, "changed": false
POST …/unsuspendNot suspended → 200, "changed": false
POST …/planAlready on that plan → 200, "changed": false
POST …/passwordSets the same password again (harmless; ends open sessions)
POST …/terminateSecond call → 404 ACCOUNT_NOT_FOUND (the account is gone)

Audit

Every call — reads included — is written to the panel audit log (Security › Audit Log) with the token id, the token owner, the client IP and the route. Request bodies are redacted (password never appears). Password changes, terminations and SSO tickets are also written to the panel log.

Endpoints

GET /test

TestConnection.

{
  "ok": true,
  "api": "arenapanel-provisioning",
  "apiVersion": "1",
  "panelVersion": "1.7.0",
  "owner": { "username": "admin", "role": "admin" },
  "token": { "id": 12, "name": "WHMCS" }
}

GET /plans

Plans this token may assign: for an administrator, the active server plans; for a reseller, its own plans plus the administrator plans it was granted.

{ "plans": [ {
  "id": 10, "name": "Basic", "description": null,
  "diskMB": 1000, "bandwidthMB": null,
  "maxSites": 1, "maxDatabases": 2, "maxFtpAccounts": 1,
  "maxSubdomains": 10, "maxAddonDomains": 0,
  "maxMailboxes": 10, "maxMailDomains": 1,
  "phpVersions": ["83"]
} ] }

POST /accounts

Create an account through the same code path as the admin panel (or the reseller panel for reseller tokens): same validation, plan copy-on-assign, reseller ownership and quota checks.

{
  "domain": "shop.example.org",
  "username": "shopexam",
  "password": "…",
  "email": "owner@example.org",
  "plan": "Basic",
  "accountType": "hosting",
  "phpVersion": "83"
}
FieldRequiredNotes
domainyesLowercase FQDN
emailyesAccount contact
planhosting: yes; mail: optionalid or exact name
usernameno^[a-z][a-z0-9_-]{0,15}$; derived from the domain when omitted
passwordno8–128 chars. If omitted the panel generates one and returns it once as generatedPassword
accountTypenohosting (default) or mail (administrator tokens only; the plan, if given, sets mailbox limits)
phpVersionnoe.g. 83; must be installed and allowed by the plan

201 when created, 200 when the account already existed (idempotent retry):

{ "created": true, "account": { …account object… }, "warnings": [] }

A password you sent is never echoed back.

GET /accounts/:username

{ "account": {
  "username": "shopexam",
  "domain": "shop.example.org",
  "email": "owner@example.org",
  "accountType": "hosting",
  "status": "active",
  "suspended": false,
  "suspendedReason": null,
  "plan": { "id": 10, "name": "Basic" },
  "disk": { "usedMB": 321, "limitMB": 1000 },
  "bandwidth": { "usedMB": 12, "limitMB": 20000, "month": "2026-11" },
  "createdAt": "2026-11-07T10:00:00.000Z"
} }

status is active, suspended or disabled.

GET /usage

All accounts the token owns, for usage sync (WHMCS UsageUpdate).

{ "month": "2026-11", "generatedAt": "…", "accounts": [ { …account object… } ] }

POST /accounts/:username/suspend

Body: { "reason": "Overdue on Invoice #42" } (optional, max 255 chars). Websites show the suspended page, PHP pools stop, FTP / database / shell / panel logins are blocked. Nothing is deleted. A reseller suspension is marked as such; resellers cannot lift an administrator's suspension.

POST /accounts/:username/unsuspend

Reverses the suspension exactly.

POST /accounts/:username/terminate

Body: { "confirm": true } (a JSON boolean; anything else is CONFIRM_REQUIRED).

Uses the panel's safe termination: a safety archive (every database dump, the home directory, each maildir and the crontab) is written first, and if any part of it cannot be written nothing is deleted. The archive is kept on the server for the panel's retention period. If the OS user cannot be removed the account is kept (suspended, annotated) and the call answers 422 OPERATION_FAILED.

{ "terminated": true, "username": "shopexam", "safetyArchive": "/root/terminated-accounts/shopexam-…", "warnings": [] }

POST /accounts/:username/plan

Body: { "plan": "Pro" }. Applies the plan's limits (copy-on-assign) and re-applies the disk quota; reseller quotas are checked. For mail-only accounts only the mailbox limits are taken from the plan.

POST /accounts/:username/password

Body: { "password": "…" } (8–128 chars). Sets the account's panel password and ends all of its open sessions and API tokens. The response never contains the password: { "changed": true, "username": "shopexam" }.

POST /accounts/:username/sso

Single sign-on into the customer's own client panel.

{
  "url": "https://panel.example.com:2083/?imp=3f1c…",
  "expiresInSeconds": 60,
  "expiresAt": "2026-11-07T10:01:00.000Z",
  "singleUse": true
}

Flow:

  1. The billing system calls POST …/sso and redirects the customer's browser to url.
  2. The URL carries only an opaque 32-byte ticket — never a session token. The ticket is valid for 60 seconds, once, and only on the client tier (2083).
  3. The panel's front end immediately strips the ticket from the address bar and redeems it with POST /api/auth/impersonation/exchange (body, not URL). A ticket redeemed on the wrong port, a second time, after 60 s, after the account was suspended, or after its password changed is refused.
  4. The resulting session is the customer's own panel, marked as delegated by the token owner (audited as a billing SSO, 2-hour limit). From such a session the password, 2FA and API tokens cannot be changed — the customer does that from a normal login.

Suspended or disabled accounts answer 409 ACCOUNT_SUSPENDED.

Example

TOKEN=ap_…
BASE=https://panel.example.com:2087/api/provisioning/v1

curl -sH "Authorization: Bearer $TOKEN" $BASE/test
curl -sH "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
     -d '{"domain":"shop.example.org","email":"o@example.org","password":"S3cret-Pass-123","plan":"Basic"}' \
     $BASE/accounts
curl -sH "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
     -d '{"confirm":true}' $BASE/accounts/shopexam/terminate