cPanel accounts
Account Scope Required — include the X-Auth-Account header with your
Account ID. Reads require the cpanel:read OAuth scope. Every write
on this page is unavailable via OAuth: ordering, resizing and cancelling
move money, and changing the password, purging, managing domains and opening
a session each hand over control of the hosting itself. All of them declare
no scope and are fail-closed to 403 endpoint not available via OAuth for
any OAuth token, whatever scopes it carries — the same treatment
POST /api/orders and registrant_change
get. Use a session or API-key credential for those.
See OAuth and
Authentication.
Availability is enabled per workspace
cPanel hosting is not available on every workspace. It is enabled for
workspaces that are already cPanel customers — one that is not returns
403 cpanel_not_enabled on every endpoint on this page, including the
read endpoints.
Reads join the hosting platform's account mirror with the local record
CloudPress keeps for each account, so a value can lag WHM by a few minutes (see
local_package below). Writes run through the same cart and checkout
machinery the dashboard uses, so there is exactly one provisioning, pricing and
billing path — and, like the other paid endpoints, they return 202 Accepted
with the shared cart polling envelope rather than a finished result. See
Async Operations.
Addressed by username, not a GUID
Unlike the rest of this API, a cPanel account is identified by its cPanel
username — GET /api/cpanel_accounts/acmeco01, not a GUID. The path
segment is constrained to the WHM username charset (letters, digits and
underscores); anything else does not reach the lookup.
Request guards
Applied in this order, before the action runs. The first one that fails answers the request.
| Applies to | Condition | Response |
|---|---|---|
| all | X-Auth-Account missing or not resolvable |
400 missing_account |
| all | cPanel hosting is not configured on the platform | 503 cpanel_unavailable |
| all | workspace is not a cPanel customer | 403 cpanel_not_enabled |
| create, update, destroy | credential has no user (a system Account-bearer API key) | 401 user_required |
| create, update | workspace is on a trial | 403 trial_account |
| show, update, destroy | username unknown, or not yours | 404 account_not_found |
| show, update, destroy | the account mirror could not be read during the lookup | 503 cpanel_unavailable |
| create, update | user cannot manage billing on the owning workspace | 403 not_authorized |
| destroy | user cannot manage service lifecycle on the owning workspace | 403 not_authorized |
| update | account is pending deletion | 403 pending_deletion |
| update | account was suspended by staff | 403 account_suspended |
| update | account is suspended for an unpaid invoice | 402 service_suspended |
Why a missing header is a 400, not a 403
The account-scope check deliberately runs before the availability gate,
so a request that simply forgot X-Auth-Account gets the standard
400 missing_account instead of cpanel_not_enabled — which would read
as "your workspace cannot buy cPanel" when the real problem is the header.
Role checks run against the owning workspace
Access can be granted to a user directly, so the account you are addressing
is not always owned by the workspace in X-Auth-Account. Billing and
lifecycle roles are therefore checked on the workspace that owns the
account, and a resize prorates that workspace's subscription. Being a
billing manager in one workspace does not let you resize another's account.
An unauthorized username answers 404 account_not_found, not 403 — the API
does not confirm that a username exists elsewhere on the platform. The three
update state guards run in the order shown above, so when more than one holds
you get the most actionable answer: recover it, then contact support, then pay
the invoice. The pending_deletion body also carries delete_scheduled_at.
show is deliberately not state-gated: a suspended or pending-deletion
account can still be read.
Per-account endpoints
The password, purge, domain and session endpoints are nested under one account,
so they all run the same first four guards — missing_account,
cpanel_unavailable, cpanel_not_enabled, then the lookup and tenancy check
that answer 404 account_not_found. On top of that:
| Endpoint | Needs a user | Trial | Role on the owning workspace | Locked while pending deletion |
|---|---|---|---|---|
GET …/domains |
no | allowed | none | no |
POST …/domains |
yes | rejected | billing management | yes |
DELETE …/domains/{domain} |
yes | rejected | billing management | yes |
PATCH …/password |
yes | rejected | billing management | yes |
POST …/purge |
yes | rejected | service lifecycle | n/a — it requires pending deletion |
POST …/session |
yes | rejected | edit | no |
"Needs a user" means a system Account-bearer API key is refused with 401
user_required; a trial workspace gets 403 trial_account; a failed role
check gets 403 not_authorized.
Unlike PATCH /api/cpanel_accounts/{username}, none of these carry the
suspension guards — there is no account_suspended or service_suspended
answer on them.
Why the domain list is the loosest of the six
GET …/domains requires no user and no role because
View a cPanel Account doesn't either. Making an
account's domain list harder to read than the account it belongs to would be
an asymmetry with nothing behind it.
A pending-deletion account is locked
Changing the password or adding or removing a domain on an account that is
scheduled for deletion answers 403 pending_deletion, with
delete_scheduled_at in the body. Restore the account first — which is a
dashboard action (see Still dashboard-only), so an
unattended job should alert on this rather than retry.
A cancelled account is also suspended on the hosting server, so don't plan on reaching what's inside it — restore it first.
There is no API step-up authentication
The dashboard re-confirms your identity before a password change, a purge or
a domain removal. The API has no equivalent prompt, so a first-class
credential — session or API key, never OAuth — plus the role check above
stands in for it. That is the same substitution
DELETE /api/cpanel_accounts/{username},
DELETE /api/sites/{id} and
GET /api/domain_registrations/{id}/epp_code
already make.
List cPanel Accounts
GET /api/cpanel_accounts
Params (optional)
- page: Integer | page number (default: 1)
- per_page: Integer | records per page (default: 50, max: 100)
- q: String | case-insensitive substring search across username, server hostname, primary domain, and add-on/alias domains
Returned Params
- cpanel_accounts: Array
- username: String
- primary_domain: String |
nullwhen the account has no main domain recorded yet - package: Object | the live WHM package
- code: String | the live WHM package code, uppercased. Commonly
S,M,LorXL, but not limited to those — whatever WHM reports is passed through unfiltered, so legacy and out-of-catalogue codes appear here verbatim.nullwhen WHM reports no package - key: String |
mini,basic,pro,max;nullfor a package not in the current catalogue - label: String | e.g.
Pro; falls back to the raw code
- code: String | the live WHM package code, uppercased. Commonly
- local_package: String | WHM code. Present only while the package CloudPress last provisioned disagrees with the live mirror — i.e. a create or resize WHM has applied but the mirror has not synced yet. Omitted the rest of the time
- disk: Object
- used_mb: Integer
- quota_mb: Integer
- server: String | normalized hostname, or
null - mailbox_count: Integer
- addon_domain_count: Integer
- state: String |
active,provisioning,dunning_suspended,admin_suspended,pending_deletion - delete_scheduled_at: DateTime |
nullunless the account is scheduled for deletion - subscription: Object |
nullwhen the account has no local subscription- id: String | subscription GUID
- status: String
- term: String | the subscription's current term
- account: Object | the owning workspace. Omitted when the account has no local record (a customer-level grant)
- id: String
- name: String
state and the provisioning window
state is derived, and pending_deletion wins over both suspension flags —
it is the terminal state and the only one that changes which operations are
possible. An account you just ordered appears as provisioning until WHM
finishes creating it; those rows are built from the local record, so
disk.used_mb / disk.quota_mb and server are null, mailbox_count
and addon_domain_count are 0, local_package is never present, and
package reflects the package you ordered rather than a live reading.
500-account ceiling
At most 500 accounts are reachable in total, however you page. The
workspace's account lookup is capped at 500 and pages are taken from that
capped set, so a workspace with more than 500 cPanel accounts cannot reach
the remainder through this endpoint — later pages simply run out. Narrow
the result with q instead of paging deeper. (The dashboard list runs under
the same cap.)
The response carries no total-count metadata; page until cpanel_accounts comes
back empty, subject to the ceiling above.
What is not listed
Only cPanel hosting accounts appear. Mail-only accounts are excluded:
they are not cPanel hosting accounts, and show / update / destroy all
answer 404 for them, so listing them would advertise usernames nothing
else on this page can address. Accounts whose local record was already
purged are excluded too, even if the mirror still returns them.
Errors
- 400
missing_account| no workspace resolved fromX-Auth-Account - 403
cpanel_not_enabled| cPanel hosting is not enabled for this workspace - 503
cpanel_unavailable| the account mirror could not be read
This endpoint fails loudly
When the mirror is unreachable the dashboard degrades to a banner and still
renders the page. The API does the opposite and reports the failure
explicitly as 503 cpanel_unavailable, so a client never mistakes an
outage for "you have no cPanel accounts". Retry rather than reconciling
against an empty list.
View a cPanel Account
GET /api/cpanel_accounts/:username
Returns the same object as the list, under a cpanel_account key.
Returned Params
- cpanel_account: Object | see List cPanel Accounts
Counts and the primary domain are read from the cached mirror and degrade rather
than fail: if a figure cannot be read, mailbox_count comes back as 0 and
primary_domain falls back to the domain recorded locally.
Errors
- 400
missing_account| no workspace resolved fromX-Auth-Account - 403
cpanel_not_enabled| cPanel hosting is not enabled for this workspace - 404
account_not_found| unknown username, or not accessible to this credential - 503
cpanel_unavailable| the account mirror could not be read
Order a cPanel Account
POST /api/cpanel_accounts
This charges immediately
cPanel hosting is payment-first. This endpoint charges the billing
account's saved default payment method off-session and provisions
asynchronously. There is no synchronous success branch and no checkout
redirect — it always returns 202 Accepted. The WHM account (username and a
generated password) is created once payment settles.
Requires a user-scoped credential; a system Account-bearer API key
returns 401 user_required. Trial workspaces return 403
trial_account. Not available via OAuth.
Params
- package: String (required) | package key (
mini,basic,pro,max) or WHM code (S,M,L,XL) - term: String (optional) |
monthlyorannual; defaults to the workspace's billing term - domain: String (optional) | a domain the workspace already owns, used as the account's main domain. Omit it and cPanel generates a placeholder domain
- callback: Object (optional) | outbound notification when the async work completes — same contract as
POST /api/orders, see Callbacks- authorization: String | full Authorization header value. Example:
Bearer 12345 - url: String | fully qualified URL
- authorization: String | full Authorization header value. Example:
Registering a new domain is a separate order
domain must already be a domain in the workspace and actually
registered — a name merely attached to a site is not enough. Registering a
domain in the same cart is not supported here; call
POST /api/orders/domain first,
then order the hosting account.
curl -X POST https://my.cloudpress.com/api/cpanel_accounts \
-H "Authorization: Bearer $CLOUDPRESS_TOKEN" \
-H "X-Auth-Account: $ACCOUNT_ID" \
-H "Content-Type: application/json" \
-d '{"package": "pro", "term": "monthly", "domain": "acmeco.com"}'
Returned Params (always 202 Accepted)
- status: String |
"accepted" - cart: Object
- token: String
- status: String | the cart's own status. Commonly
active,processingorchecked_out; while you are polling it can also becomeawaiting_provisioning(payment captured, provisioning parked) orexpired(the cart was failed or abandoned), so treat the list as examples rather than a closed set - rollup_status: String
- poll_url: String | absolute URL, e.g.
https://your-instance/api/carts/<token>
- payment: Object
- status: String |
processing,succeeded,awaiting_authentication - method_type: String | the resolved Stripe payment-method type. Commonly
cardorsepa_debit, but any Stripe type can land here —us_bank_account,bacs_debit,acss_debit,au_becs_debit,customer_balance,idealand others are all possible.nulluntil it can be resolved - hosted_invoice_url: String |
null
- status: String |
- orders: Array | each
{ id, status, poll_url }; empty during the async window
Poll cart.poll_url to discover the order, then each orders[].poll_url until
its status is terminal. orders is empty at first on Stripe billing plans — the
order is created after the charge settles. Payment scenarios (3DS parking, SEPA,
credit balance, decline) behave exactly as documented for
PATCH /api/sites/:id.
Errors
Authorization / gating:
- 400
missing_account| no workspace resolved fromX-Auth-Account - 401
user_required| system Account-bearer key (no user) - 403
cpanel_not_enabled| cPanel hosting is not enabled for this workspace - 403
trial_account| trial workspaces cannot buy cPanel hosting - 403
not_authorized| user cannot manage billing on this workspace
Validation (400, nothing charged, no order created):
unknown_package|packageis not a known package key or WHM codeinvalid_term|termis notmonthlyorannualproduct_unavailable| the requested package is not on this billing planno_default_payment_method| the billing account is not ready to charge
Processing (422):
no_price_for_plan| no price for the requested package and termdomain_not_owned|domainis not a domain in this workspacedomain_not_registered|domainis not registered anywheredomain_in_use|domainis already live on another hosting accountcart_add_failed| the cart rejected the item-
cart_pay_failed| the off-session charge could not be started -
503
cpanel_unavailable| cPanel is not configured, ordomaincould not be verified against the hosting servers
Change Package (Resize)
PATCH /api/cpanel_accounts/:username
Changes the WHM package and re-prorates billing. On a Stripe billing plan this
is a proration cart charged off-session — the same machinery as a WordPress site
resize; on a non-Stripe plan it builds a resize order that applies the package
change with no charge. Returns 202 Accepted with the same cart envelope as
Order a cPanel Account.
The term never changes
A resize is always priced on the live subscription's current term. There
is no term, domain or callback param here, so this endpoint never
returns invalid_term, domain_not_owned or domain_in_use.
Params
- package: String (required) | target package key (
mini,basic,pro,max) or WHM code (S,M,L,XL)
Returned Params (always 202 Accepted)
Identical envelope to Order a cPanel Account:
- status: String |
"accepted" - cart: Object |
{ token, status, rollup_status, poll_url } - payment: Object |
{ status, method_type, hosted_invoice_url } - orders: Array |
[{ id, status, poll_url }](empty during the async window)
Do not retry a resize blind
A package change is claimed before any money moves, so a second PATCH sent
while the first is still applying is refused with 409 resize_in_flight
rather than charging a second proration. "Already on that package" is
checked against both the live mirror and the package last provisioned,
which closes the same window from the other side. If a request times out,
poll the cart or re-read the account instead of re-sending the PATCH.
Downgrades must fit current usage
A smaller package is refused with 422 usage_exceeds_package when the
account's current disk, mailbox or add-on-domain usage exceeds what the
target includes — WHM would otherwise apply the smaller quota anyway and
leave the account instantly over quota. Reduce usage first. Upgrades are
never usage-checked, so an account sitting above today's caps on an older
package can always move up.
Errors
Authorization / gating:
- 400
missing_account| no workspace resolved fromX-Auth-Account - 401
user_required| system Account-bearer key (no user) - 403
cpanel_not_enabled| cPanel hosting is not enabled for this workspace - 403
trial_account| trial workspaces cannot resize - 403
not_authorized| user cannot manage billing on the owning workspace - 404
account_not_found| unknown username, or not accessible to this credential
Account state:
- 403
pending_deletion| the account is pending deletion and locked — recover it first (body carriesdelete_scheduled_at) - 403
account_suspended| suspended by staff; contact support - 402
service_suspended| suspended for an unpaid invoice
Validation (400):
unknown_package|packageis not a known package key or WHM codeproduct_unavailable| the target package is not on this billing planno_default_payment_method| a saved default payment method is required to charge the proration off-session, and none was found
Conflict (409):
resize_in_flight| an earlier package change is still being applied
Processing (422):
same_package| already on that packageusage_exceeds_package| the target package is smaller than current disk / mailbox / add-on-domain usageno_price_for_plan| no price for the target package on the subscription's termno_billing_subscription| Stripe billing plan with no linked subscription to prorate against (an account migrated in and never billed here)subscription_not_proratable| the cart could not be staged as a proration; refused rather than charged as a brand-new purchasecart_add_failed| the cart rejected the item-
cart_pay_failed| the off-session charge could not be started -
503
cpanel_unavailable| cPanel is not configured, or the mirror could not be read
Cancel a cPanel Account
DELETE /api/cpanel_accounts/:username
Soft-delete. This stamps the retention window, cancels billing — stopping
renewals and crediting the unused first term — and suspends the account on WHM.
The account stays recoverable from the CloudPress dashboard until
delete_scheduled_at, when it is removed permanently.
Gated on service-lifecycle role membership of the owning workspace. Staff who are not members of the workspace are excluded, and there is no API step-up authentication that could stand in for the dashboard's re-confirm-identity prompt.
Returns 202 Accepted.
Returned Params
- status: String |
"pending_deletion" - username: String
- delete_scheduled_at: DateTime | the frozen purge date; recoverable until then
Errors
- 400
missing_account| no workspace resolved fromX-Auth-Account - 401
user_required| system Account-bearer key (no user) - 403
cpanel_not_enabled| cPanel hosting is not enabled for this workspace - 403
not_authorized| user cannot manage service lifecycle on the owning workspace - 404
account_not_found| unknown username, or not accessible to this credential - 422
account_already_purged| the mirror still lists the account but it has already been purged - 422
account_delete_failed| the account could not be scheduled for deletion - 503
cpanel_unavailable| cPanel is not configured, or the account's server credentials could not be resolved
Purge a Pending-Deletion Account
POST /api/cpanel_accounts/:username/purge
Permanent and irreversible. Releases the account and its data now instead of
at delete_scheduled_at. This only shortens a retention window that
cancelling already opened — the account must
already be pending deletion, and there is no combined cancel-and-purge path,
so a single mistaken call can never destroy a live account.
Returns 202 Accepted. The removal itself runs in the background, but the
account leaves GET /api/cpanel_accounts immediately — its absence from that
list is the signal to poll for, and there is no task to follow.
Gated on service-lifecycle role membership of the owning workspace, and unavailable via OAuth.
Returned Params
- status: String |
"purging" - username: String
Errors
- 400
missing_account| no workspace resolved fromX-Auth-Account - 401
user_required| system Account-bearer key (no user) - 403
cpanel_not_enabled| cPanel hosting is not enabled for this workspace - 403
trial_account| the workspace is on a trial - 403
not_authorized| user cannot manage service lifecycle on the owning workspace - 404
account_not_found| unknown username, or not accessible to this credential - 422
account_not_pending_deletion| the account is live; cancel it first - 503
cpanel_unavailable| cPanel is not configured, or the mirror could not be read
Change the cPanel Password
PATCH /api/cpanel_accounts/:username/password
Sets the password on the hosting account immediately — there is no async window, so the old password stops working at once.
This is not a session kill switch
Whether cPanel also tears down sessions already open under the old password is cPanel's own behaviour and is not guaranteed here. If you are rotating because a credential leaked, do not treat this call as proof the leaked session is closed.
Password strength is enforced by the hosting server, not by CloudPress; a value
it rejects comes back as password_change_failed. The failure text is
deliberately generic — the server's own message is logged rather than echoed.
Params
- password: String (required) | the new password. There is no
password_confirmation— that is a form affordance, not an API contract
Returned Params
- status: String |
"changed" - username: String
Errors
- 400
missing_account| no workspace resolved fromX-Auth-Account - 401
user_required| system Account-bearer key (no user) - 403
cpanel_not_enabled| cPanel hosting is not enabled for this workspace - 403
trial_account| the workspace is on a trial - 403
not_authorized| user cannot manage billing on the owning workspace - 403
pending_deletion| the account is scheduled for deletion; restore it first - 404
account_not_found| unknown username, or not accessible to this credential - 422
password_blank| nopasswordsupplied - 422
password_change_failed| the server refused the value, usually on strength - 503
cpanel_unavailable| cPanel is not configured, or the account's server credentials could not be resolved
List Account Domains
GET /api/cpanel_accounts/:username/domains
The aliases, subdomains and add-on domains attached to one account. Read live from the hosting server (falling back to the local mirror), so a domain added through this API appears as soon as the server has finished building it.
Each row carries two type fields. type is the raw value the server reports and
is schema-dependent — main, addon, sub, parked, alias and other
spellings all occur. domain_type is the normalized vocabulary this API speaks
and is what the rest of this section means by a domain's kind; it is null for
the account's main domain, which cannot be removed.
Returned Params
- domains:
Array<Object>- name: String
- type: String | the raw type the hosting server reports;
nullif it reports none - domain_type: String |
parked,subdomainoraddon;nullfor the main domain
- usage: Object | per-kind counters and the package's caps
- addon: Object
- count: Integer
- limit: Integer | the numeric cap;
nullwhen unlimited or unknown - unlimited: Boolean |
trueonly when the server actually said unlimited - at_limit: Boolean |
trueonly against a real numeric cap, so an unknown limit never blocks
- alias: Object | same shape as
addon - subdomain: Object | same shape as
addon
- addon: Object
limit: null is ambiguous on purpose
A null limit means either "unlimited" or "we could not read the cap" —
unlimited is the field that distinguishes them. Branch on at_limit, which
is only ever true against a cap that is genuinely known.
Errors
- 400
missing_account| no workspace resolved fromX-Auth-Account - 403
cpanel_not_enabled| cPanel hosting is not enabled for this workspace - 404
account_not_found| unknown username, or not accessible to this credential - 503
cpanel_unavailable| cPanel is not configured, or the domain list could not be read
Add a Domain
POST /api/cpanel_accounts/:username/domains
Which parameters are required depends on domain_type:
Pass the full hostname in domain. It must already be registered and owned
by this workspace — a name that is not answers the deliberately generic
domain_not_eligible rather than explaining which half failed, so this
cannot be used to probe how a domain is known on the platform.
An alias's share target is not a parameter; it always resolves server-side to the account's primary domain.
Pass label (a single DNS label, no dots) and rootdomain. The parent
must be one of this account's own domains — its main, add-on or
parked/alias domains are all eligible.
Returns 202 Accepted, not 201, and the difference matters: the hosting
server reports success before the vhost and zone rebuild finishes, so the domain
can be missing from List Account Domains for a short
while afterwards. Poll that list for the name rather than treating the response
as proof the domain is live.
Params
- domain_type: String (required) |
parked,subdomainoraddon - domain: String (required for
parkedandaddon) | the full hostname - label: String (required for
subdomain) | a single DNS label, no dots - rootdomain: String (required for
subdomain) | the parent domain - dir: String (optional,
subdomainandaddon) | document root
Returned Params
- status: String |
"accepted" - domain: String
- domain_type: String
Errors
- 400
missing_account| no workspace resolved fromX-Auth-Account - 401
user_required| system Account-bearer key (no user) - 403
cpanel_not_enabled| cPanel hosting is not enabled for this workspace - 403
trial_account| the workspace is on a trial - 403
not_authorized| user cannot manage billing on the owning workspace - 403
pending_deletion| the account is scheduled for deletion; restore it first - 404
account_not_found| unknown username, or not accessible to this credential - 422
invalid_domain_type|domain_typeis not one of the three - 422
domain_blank| nodomainsupplied - 422
invalid_hostname|domainis not a valid hostname - 422
label_blank| nolabelsupplied for a subdomain - 422
invalid_label|labelis not a valid DNS label - 422
rootdomain_blank| norootdomainsupplied for a subdomain - 422
invalid_parent_domain| the parent is not one of this account's own domains - 422
domain_not_eligible| the domain is not registered to this workspace, or cannot be added here - 422
addon_domain_limit_reached| the package's add-on cap is reached - 422
alias_domain_limit_reached| the package's alias cap is reached - 422
domain_create_failed| the hosting server refused the change - 503
cpanel_unavailable| cPanel is not configured, or the server could not be reached
Remove a Domain
DELETE /api/cpanel_accounts/:username/domains/:domain
:domain is the domain name, not an ID. It must be one of this account's own
domains; anything else answers 404 domain_not_found — the same answer an
entirely unknown name gets, so this cannot be used to probe what lives elsewhere
on the platform.
The kind of removal is derived server-side from the domain's own listed row.
There is no domain_type parameter and one supplied is ignored. The account's
main domain cannot be removed (invalid_domain_type) — cancel the account
instead.
DNS is only cleaned up for subdomains
Removing a subdomain also strips its records from the parent zone. Alias and add-on removals deliberately leave the zone in place so the domain can be reused.
Params (optional)
- rootdomain: String | a hint for a subdomain's parent. Honoured only when it is genuinely one of this account's own parents; otherwise the server-side derivation wins
Returned Params
- status: String |
"removed" - domain: String
- domain_type: String
Errors
- 400
missing_account| no workspace resolved fromX-Auth-Account - 401
user_required| system Account-bearer key (no user) - 403
cpanel_not_enabled| cPanel hosting is not enabled for this workspace - 403
trial_account| the workspace is on a trial - 403
not_authorized| user cannot manage billing on the owning workspace - 403
pending_deletion| the account is scheduled for deletion; restore it first - 404
account_not_found| unknown username, or not accessible to this credential - 404
domain_not_found| the domain is not on this account - 422
invalid_domain_type| the domain is the account's main domain, or its kind could not be derived - 422
domain_remove_failed| the hosting server refused the change - 503
cpanel_unavailable| cPanel is not configured, or the server could not be reached
Open a cPanel Session
POST /api/cpanel_accounts/:username/session
Mints a one-time, short-lived login URL for the account's cPanel interface.
Like POST /api/sites/{id}/sso, it returns the URL and never
redirects, so no response from this API is ever a cross-host 302 and the caller
decides what to do with it.
Treat the URL as a credential
Anyone holding it is logged in as the cPanel account. Do not log it, cache it, or put it in a URL that gets recorded.
The returned URL is checked to be on the same hosting server the request was
issued to. One that is not is refused with sso_url_rejected and never handed
back, so a misconfigured or compromised server cannot turn this into an open
redirect.
Gated on edit permission on the owning workspace — view-only members cannot open a session.
Returned Params
- url: String | the one-time login URL
Errors
- 400
missing_account| no workspace resolved fromX-Auth-Account - 401
user_required| system Account-bearer key (no user) - 403
cpanel_not_enabled| cPanel hosting is not enabled for this workspace - 403
trial_account| the workspace is on a trial - 403
not_authorized| user cannot edit the owning workspace - 404
account_not_found| unknown username, or not accessible to this credential - 422
sso_url_rejected| the hosting server returned a login URL that could not be trusted - 503
cpanel_unavailable| the server could not be reached, or a session could not be created
Still dashboard-only
One thing on the cPanel Accounts page has no API route: restoring an account that is pending deletion. Cancelling and purging are both scriptable; undoing a cancellation is not.
That is less of an omission than it looks. Cancelling an account cancels its subscription outright and credits the unused term, so recovery is a re-purchase — it builds a cart for the package and settles a payment, then revives the existing hosting account rather than provisioning a new one. It is a checkout flow, not a single call.
That leaves a real asymmetry to plan around. Cancelling and purging are both scriptable, purging is irreversible, and undoing either one needs a person at a checkout. There is no automated escape hatch, so an unattended cleanup job should treat every cancellation it issues as final.
Need it on the API?
Tell us what you're automating — a concrete workflow is more useful to us than a general request for parity.