Skip to content

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 usernameGET /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 | null when 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, L or XL, but not limited to those — whatever WHM reports is passed through unfiltered, so legacy and out-of-catalogue codes appear here verbatim. null when WHM reports no package
      • key: String | mini, basic, pro, max; null for a package not in the current catalogue
      • label: String | e.g. Pro; falls back to the raw code
    • 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 | null unless the account is scheduled for deletion
    • subscription: Object | null when 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 from X-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

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 from X-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) | monthly or annual; 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

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, processing or checked_out; while you are polling it can also become awaiting_provisioning (payment captured, provisioning parked) or expired (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 card or sepa_debit, but any Stripe type can land here — us_bank_account, bacs_debit, acss_debit, au_becs_debit, customer_balance, ideal and others are all possible. null until it can be resolved
    • hosted_invoice_url: String | null
  • 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 from X-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 | package is not a known package key or WHM code
  • invalid_term | term is not monthly or annual
  • product_unavailable | the requested package is not on this billing plan
  • no_default_payment_method | the billing account is not ready to charge

Processing (422):

  • no_price_for_plan | no price for the requested package and term
  • domain_not_owned | domain is not a domain in this workspace
  • domain_not_registered | domain is not registered anywhere
  • domain_in_use | domain is already live on another hosting account
  • cart_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 domain could 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 from X-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 carries delete_scheduled_at)
  • 403 account_suspended | suspended by staff; contact support
  • 402 service_suspended | suspended for an unpaid invoice

Validation (400):

  • unknown_package | package is not a known package key or WHM code
  • product_unavailable | the target package is not on this billing plan
  • no_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 package
  • usage_exceeds_package | the target package is smaller than current disk / mailbox / add-on-domain usage
  • no_price_for_plan | no price for the target package on the subscription's term
  • no_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 purchase
  • cart_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 from X-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 from X-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 from X-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 | no password supplied
  • 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; null if it reports none
    • domain_type: String | parked, subdomain or addon; null for the main domain
  • usage: Object | per-kind counters and the package's caps
    • addon: Object
      • count: Integer
      • limit: Integer | the numeric cap; null when unlimited or unknown
      • unlimited: Boolean | true only when the server actually said unlimited
      • at_limit: Boolean | true only against a real numeric cap, so an unknown limit never blocks
    • alias: Object | same shape as addon
    • subdomain: Object | same shape as addon

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 from X-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, subdomain or addon
  • domain: String (required for parked and addon) | 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, subdomain and addon) | document root
Returned Params
  • status: String | "accepted"
  • domain: String
  • domain_type: String
Errors
  • 400 missing_account | no workspace resolved from X-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_type is not one of the three
  • 422 domain_blank | no domain supplied
  • 422 invalid_hostname | domain is not a valid hostname
  • 422 label_blank | no label supplied for a subdomain
  • 422 invalid_label | label is not a valid DNS label
  • 422 rootdomain_blank | no rootdomain supplied 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 from X-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 from X-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.