---
title: cPanel / WHM
description: "RackHelm creates, suspends, unsuspends and terminates cPanel accounts on your WHM server, and surfaces the resulting site on the customer's **Sites** page. This article is for the operator who sets that up: what you need from WHM, every field on…"
url: "https://rackhelm.com/documentation/cpanel-whm-integration/"
updated: "2026-09-24"
category: "Integrations: Control Panels"
---

# cPanel / WHM

RackHelm creates, suspends, unsuspends and terminates cPanel accounts on your WHM server, and surfaces the resulting site on the customer's **Sites** page. This article is for the operator who sets that up: what you need from WHM, every field on the RackHelm form, how a product is bound to a WHM package, what provisioning actually writes, and the traps worth knowing before the first paid order lands.

## Before you start

- The `edit_integrations` permission. See [Roles and Permissions](/docs/roles-and-permissions/).
- Your WHM hostname, reachable from the RackHelm server. WHM normally listens on port **2087**.
- A WHM API token created under **WHM » Development » Manage API Tokens**, and the name of the WHM user it belongs to. The token is sent as `Authorization: whm <user>:<token>`, so the username is part of the credential, not decoration.
- That user must be allowed to create, suspend and terminate accounts. A reseller token works, with the limits described below; a root token sees everything.

> **Note:** A base URL that resolves to a private, loopback or reserved address is refused with "The connection.base url must be a valid, publicly accessible URL. Internal or reserved IP addresses are refused unless listed in SAFEURL_PRIVATE_ALLOWLIST in the portal's .env file." Since 1.0.1.1 you can allow a WHM box on your own LAN by listing it there, for example `SAFEURL_PRIVATE_ALLOWLIST="192.168.1.50,10.10.0.0/16"`. Every other private address stays blocked. The reasoning is in [Adding a Control Panel Connection](/docs/adding-a-panel-connection/).

> **Note:** RackHelm stores *one* cPanel connection, not a list. The form saves into a single row keyed on the provider name, so one RackHelm install drives one WHM server. The **Panel** dropdown on a product exists because other panel types share the form; for cPanel it will only ever offer that one entry.

## Add the connection

1. Open **Admin → Integrations** and press **Configure** on the **cPanel** card (it reads **Manage** once a connection exists).
2. Set **Base URL** to your WHM origin including the port, for example `https://cpanel.example.com:2087`.
3. Set **WHM Username** to the user the token belongs to. It defaults to `root`.
4. Paste the WHM API token into **API Token**.
5. Leave **Verify SSL** on **Yes** if WHM presents a certificate that validates for that hostname. If it is self-signed, choose **No (pin certificate)**, press **Test API** to read the certificate, then **Save**.
6. Adjust **HTTP Timeout (sec)** if your server is slow to answer, then press **Save**.

![The cPanel panel form with Name, Active, Base URL, WHM Username, API Token, HTTP Timeout, Verify SSL and the cPGuard block](/wp-content/uploads/rackhelm-docs/integrations-core-panel-edit-cpanel.png)
The cPanel connection form. The cPGuard block at the bottom is optional and unrelated to provisioning.

### Fields

| Field | Rule | Notes |
| --- | --- | --- |
| **Name** | Optional, max 120 characters | Defaults to "cPanel". Shown in the **Panel** dropdown on a product. |
| **Active** | Checkbox | Must be ticked for cPanel to appear as an **Integration Type** on products. |
| **Base URL** | Required, http/https, publicly resolvable or allow-listed | Placeholder `https://cpanel.example.com:2087`. |
| **WHM Username** | Optional, max 190 characters | "The WHM user the API token belongs to (root or a reseller)." Blank keeps the stored value; if nothing is stored the client falls back to `root`. |
| **API Token** | Optional on edit | Blank keeps the stored token. Stored encrypted. |
| **HTTP Timeout (sec)** | Integer, minimum 1 (form minimum 5) | Default 30. Applies to every call; account creation and removal override it to 180 seconds, because `createacct` legitimately runs that long. |
| **Verify SSL** | **Yes** or **No (pin certificate)** | Read by the driver, the connection test and the product-form lookups. With **No**, the server's public key is pinned and any other key is refused. |
| **cPGuard API Key** | Optional | Moved into the encrypted credential block on save, never left in the plain settings blob. Feeds the read-only CMS Threat Alerts report on the customer's site dashboard. |
| **Verify cPGuard SSL Certificate** | Checkbox, on by default | cPGuard listens on port 9098; untick for a self-signed certificate there. |

### The pinned certificate

Choosing **No (pin certificate)** reveals a line under the dropdown. Before anything is pinned it reads "No certificate is pinned yet. Click **Test API** to read the panel's certificate, then **Save** to pin it. Until then the portal refuses to connect with verification off." After a successful test it reads **Pinned certificate:** followed by a description in the shape `CN=cpanel.example.com, self-signed, expires 2027-03-14, SHA-256 A1:B2:…`, plus a **Forget pinned certificate** link.

![The Verify SSL dropdown set to No, with the pinned certificate description and the Forget pinned certificate link below it](/wp-content/uploads/rackhelm-docs/integrations-core-panel-ssl-pin.png)
The pin is read by **Test API** and stored by **Save**. Testing alone does not pin anything.

> **Warning:** The pin is stored by **Save**, not by **Test API**. If you test, see "certificate pinned, click Save to keep it" and then navigate away, nothing is stored and the connection stays broken. When the certificate is later replaced — a renewed self-signed certificate, or AutoSSL taking over the hostname — calls start failing with "The panel's certificate key no longer matches the pinned one. Open the panel settings, click Test API to see the new certificate, then Save to trust it." That is the pin doing its job, not a fault.

## Testing the connection

**Test API** now goes through the same client provisioning uses. It posts `json-api/version` to WHM with your `Authorization: whm <user>:<token>` header and the TLS settings you have saved, and on success reports **OK** followed by the version WHM itself returned, for example **OK · WHM version 11.136**. A pass here means the hostname, port, certificate, firewall, WHM username and token are all correct.

> **Note:** In 1.0.0.0 this button sent a Plesk REST path to every panel and a 404 was expected. That is gone. A failure now is a real failure.

Leaving the token box blank and pressing **Test API** re-tests with the stored token — the form posts the literal string `KEEP` and RackHelm substitutes the saved value. Two deliberate refusals:

- **"Re-enter the API token when changing the Base URL."** — you edited the base URL and left the token blank. The stored token is never shown back to you, so resolving it onto a host you have just typed would be a way to have it delivered somewhere else. Paste the token again.
- **"No saved API token; please enter it."** — nothing is stored yet, so there is nothing to re-use.

When WHM answers but rejects the credentials you get a fixed message: "WHM rejected the API token. Check the WHM username and the token (WHM » Development » Manage API Tokens)." Any other failure reads "The panel rejected the connection test. See the application log for the upstream response." The wording is constant on purpose — varying it by what WHM returned would turn the button into a probe for whatever host was typed into **Base URL**. The real response is in the application log.

## Bind a product to cPanel

Open **Catalogue → Catalogue & Groups**, edit the product, and on the **Provisioning** tab find the **Server Integration** card. Set **Integration Type** to **cPanel**, pick the **Panel**, and the three dropdowns below it fill themselves from your WHM server.

![The cPanel block on a product's Provisioning tab with Panel, Package / Plan, Feature List and Owner / Reseller dropdowns and the Reload lists from panel link](/wp-content/uploads/rackhelm-docs/integrations-core-cpanel-product-config.png)
Package, feature list and reseller are read from WHM, not typed.

| Field | What it is for |
| --- | --- |
| **Panel** | Which saved cPanel connection to use. Credentials come from this row and nowhere else — a product cannot supply its own base URL or token. |
| **Package / Plan** | Required. The WHM package new accounts are created on, chosen from **WHM » Packages**. Passed to `createacct` as its `plan` parameter. "A reseller token only sees its own packages." |
| **Feature List (optional)** | From **WHM » Feature Manager**. Overrides the package's feature list for accounts created from this product. Blank means the package decides, which is WHM's own default. |
| **Owner / Reseller (optional)** | The WHM user that will own the new accounts. Blank means the API token's own user. Only a root token can list resellers. |
| **AutoSSL** | Enabled or Disabled. Stored with the product; the driver does not send it to WHM. |
| **Suspend after (days past due)** | Stored, default 1. Not read by anything — see the warning below. |
| **Terminate after (days after suspend)** | Stored, default 7. Same. |
| **Provisioning Notes (internal)** | Free text for staff. Never shown to customers. |

Press **Reload lists from panel** after adding a package in WHM; the lists are cached for 60 seconds otherwise. If a saved value is no longer on the server it stays selected and is relabelled, for example `Starter (saved value, not found on panel)`, so you can see the mismatch rather than lose it.

> **Note:** If the panel cannot be reached, each dropdown turns into a plain text box with the message "Could not load the lists from the panel: … You can type the values instead." and the placeholder "Package name exactly as shown in WHM". The product still saves. Listing packages is the call that must succeed; feature lists and resellers degrade to an empty list with a warning such as "Resellers could not be listed (the API token is not allowed to)." — which is what a reseller token always gets, because `listresellers` is root-only in WHM's ACLs.

> **Note:** **Package / Plan** is validated as required. Saving without it fails with "The integration.cpanel.package id field is required." WHM itself would fall back to the default package, but RackHelm insists you choose one.

> **Warning:** **Suspend after (days past due)** and **Terminate after (days after suspend)** are saved on the product and then ignored. The timing lives on the schedules **Suspend Overdue Services** and **Terminate Overdue Services** under **Schedules**, each with its own `days` option, and **both ship disabled**. Until you enable them nothing is ever suspended or terminated automatically, however those product fields are set. See [Suspending, Unsuspending and Terminating a Service](/docs/suspending-and-terminating-services/).

> **Warning:** The **Enable Provisioning** switch at the top of the **Server Integration** card is not written when a product is saved. Switching it off does not stop provisioning; the integration is rebuilt as active every time. To sell a product without automatic provisioning, set **Integration Type** to **None / Manual Processing** instead.

## The domain the account is created for

cPanel needs a domain name, and RackHelm takes it from the order item — the `DomainName` value the customer typed at checkout, or a `domain` value already stored on the item. Attach the seeded **Domain Name** custom field (key `DomainName`) to the product under **Options & Add-ons → Configurable Options**, or give the product a fixed saved value if every account uses the same name. See [Custom Fields](/docs/custom-fields/).

Without one, provisioning fails immediately with:

```bash
Missing DomainName (OrderItem.variable_values or product/group saved variable).
```

## What provisioning does

Nothing provisions itself: the **Provision Orders** schedule finds paid items awaiting provisioning and queues each one, and like most schedules it ships disabled — enable it under **Schedules**. See [How Automatic Provisioning Works](/docs/automatic-provisioning/). Once an item is queued the driver:

1. Resolves the domain, and the customer's email from the order's user. A user with no email address fails with "OrderItem has no user (or user has no email) linked via order."
2. Invents a username from the domain: lower-case letters and digits, starting with a letter, **at most 8 characters** including a random numeric suffix. Eight rather than WHM's current 16 keeps the name valid on older servers and clear of MySQL prefix truncation. A leading `test` is stripped, because cPanel refuses usernames that begin with it. The name is checked against WHM before use; after five collisions it gives up with "Could not find a free cPanel username after 5 attempts."
3. Generates a 16-character password containing upper case, lower case, a digit and a symbol from the set `!@*-_+=`.
4. Calls `createacct` with the username, domain, password, package, the customer's email as `contactemail`, and any feature list and owner you set.
5. Records the username as the item's external id, and writes `panel_id`, `username`, `domain`, `plan`, `featurelist`, `owner`, the assigned `ip` and the encrypted password under the `cpanel` key of the item's provisioned metadata.

Two failure modes are handled rather than surfaced. A reseller token cannot see other resellers' accounts through `listaccts`, so a username can look free and still collide at `createacct`; when WHM answers "already has an account" or "already exists" the driver picks a new name and tries once more. And re-running provisioning after a partial failure adopts the account it already created — it does not build a second one under a fresh username.

> **Warning:** The generated cPanel password is stored only as ciphertext and nothing reads it back. It is not emailed to the customer — the "Your Service is Ready!" notification carries a dashboard link and no credentials — and there is no screen that reveals it. Customers manage the account through the portal's **Sites** area. If someone needs the cPanel password itself, reset it in WHM.

> **Warning:** Provisioning is attempted **once**. A failed item is marked failed, an admin notification "Provisioning failed for order item #…" is raised, and nothing retries it on its own — the background scan only picks up items that are pending or unstarted. Clear it by hand from **Pending Provisioning** with **Retry Provisioning** or **Mark Provisioned**. See [The Pending Provisioning Queue](/docs/pending-provisioning-queue/).

## Renewal, suspension and termination

| Action | What RackHelm does |
| --- | --- |
| Renew | Nothing on the server. The driver answers "Hosting renewal is billing-only. No remote action needed." |
| Suspend | `suspendacct` with the reason "Suspended via billing panel", which is what appears in WHM's account list. |
| Unsuspend | `unsuspendacct`. |
| Terminate | Checks the account still exists, then `removeacct` with `keepdns=0`. An account already gone is reported as success: "cPanel account already removed". |

All three read the username from the order item. If that is missing — a hand-created item, or one whose metadata was cleared — they fail with "cPanel username not found on OrderItem."

## What the customer sees

A provisioned cPanel item appears under **Sites** in the client portal, and since 1.0.1.1 that area is real on cPanel, not just Enhance. The portal translates its own screens onto WHM API 1, cPanel's UAPI and, where UAPI has no equivalent, cPanel API 2. The customer gets **FTP**, **Domains** and DNS records, **Email**, **SQL** databases and users, the PHP version and FPM restart, WordPress installs, and backups. That is the full set — cPanel is the only panel besides Enhance with backups in the portal.

A few things have no cPanel equivalent and say so plainly rather than erroring:

- Changing the primary domain: "On cPanel the primary domain is fixed when the account is created. Please contact support if it needs to change."
- Forcing HTTPS: "Forcing HTTPS is not switched on from here for cPanel sites. Add the redirect in cPanel under Domains, or ask support to set it up."
- WordPress core and plugin updates are applied from the WordPress dashboard, reached with the SSO button; and WordPress is installed at the site root only, so a non-blank installation path is refused.

See [My Sites: the site dashboard](/docs/my-sites-dashboard/) for the customer's view.

## Troubleshooting

| Symptom | Cause and fix |
| --- | --- |
| "cPanel panel has Verify SSL switched off but no pinned certificate" | The upgrade case. Open the connection, **Test API**, then **Save**. |
| "The panel's certificate key no longer matches the pinned one" | WHM's certificate was replaced. **Test API** to see the new one, then **Save** to trust it. |
| "The panel presented a certificate the portal cannot verify" | Verification is on and WHM's certificate is self-signed or untrusted. Set **Verify SSL** to **No**, **Test API**, then **Save** to pin it. |
| "Missing cPanel panel settings (base_url, api_token)" | The connection was never saved, or was saved without a token. Open **Admin → Integrations → cPanel** and save it. |
| Order fails with "Missing DomainName …" | The product has no **Domain Name** configurable option and no saved value. Attach one and retry. |
| "Could not find a free cPanel username after 5 attempts." | Five generated names were all taken. Usually a domain that has been ordered repeatedly. Create the account by hand and mark the item provisioned. |
| "WHM createacct failed: …" | WHM's own reason, passed through. Quota exhausted, package missing, IP unavailable — fix it in WHM and retry. |
| Product will not save | **Package / Plan** is empty: "The integration.cpanel.package id field is required." |
| Package dropdown is empty or has become a text box | WHM could not be reached or refused the token. The message on the form says which. A reseller token sees only its own packages. |
| Resellers list is empty with a warning | Expected with a reseller token — `listresellers` is root-only. Leave **Owner / Reseller** blank. |
| cPanel missing from the Integration Type dropdown | The connection is not marked **Active**, or was never saved. |
| Base URL rejected on save | The hostname resolves to a private or reserved address, or does not resolve. Use a public hostname, or list the address in `SAFEURL_PRIVATE_ALLOWLIST`. |
| Test API says "WHM rejected the API token" | The host answered and refused the credential. Check the **WHM Username** as well as the token — the two travel together. |
| Nothing is ever suspended for non-payment | The **Suspend Overdue Services** schedule is disabled by default. The product's **Suspend after** value is not used. |
| Item stuck showing "We're setting up your service…" | The job was killed mid-run, leaving the item in *processing*. Resolve it from **Pending Provisioning**. Account creation is allowed 180 seconds and the job 270, so the worker timeout must be at least that; see [Queue Workers, Horizon and the Scheduler](/docs/queue-workers-and-scheduler/). |
