# WHMCS

> Provision CorePanel from WHMCS: installing the module, issuing the token, registering the server, and exactly which billing actions reach which part of the panel.

Source: https://www.corepanel.net/docs/whmcs/
Last updated: 2026-09-04
Part of the CorePanel documentation — https://www.corepanel.net/docs

---

The **CorePanel module for WHMCS** lets your billing system run hosting accounts on its
own: it creates them when an order is paid, suspends them when an invoice is overdue,
lifts the suspension when it is settled, moves customers between packages, resets
passwords, reports disk and bandwidth usage, and signs customers into their control panel
from your client area.

It is a thin client of [the panel's JSON-RPC API](https://www.corepanel.net/docs/api-tokens) — the same API the
panel's own interface speaks — authenticated with an API token. Nothing is decided in the
module: what a suspension closes and what a package grants is decided by the panel, so a
suspension from WHMCS and a suspension from the panel are the same act.

The module requires **CorePanel Business** on the server, and WHMCS 8.0 or newer.

![How the two systems fit together: WHMCS on the left holds the customer, the invoice and the service username; an API token in the middle is the only credential between them; CorePanel on the right holds the account. Arrows from WHMCS to CorePanel are labelled with each billing action and the API method it calls — create, suspend, unsuspend, change package, change password, usage update, terminate and sign in. One arrow points back from CorePanel to WHMCS, labelled "the account's real username", showing that the panel is the authority on the name.](https://www.corepanel.net/_astro/whmcs-module-flow.CfivCIc1.svg)

## Setting it up

### 1. Issue a token

In the panel, go to **Server → API tokens** and create one with the `provisioning` scope,
plus `terminate` if you want WHMCS to delete accounts when a service is cancelled.

```bash
corepanel api-token create "WHMCS production" --scope provisioning --scope terminate
```

The credential is shown once. Copy it.

> **Leave out `terminate` if you want a human in the loop**
>
> Without it, dunning still runs unattended — suspensions and unsuspensions work — but a
> cancellation reports an error instead of destroying the customer's data. Some hosting providers
> prefer that. See [API tokens](https://www.corepanel.net/docs/api-tokens#scopes).
### 2. Install the module

Download it and unzip it into your WHMCS root, so the module lands in
`modules/servers/corepanel/`. No other file is touched.

```bash
cd /path/to/whmcs
curl -fsSLO https://get.corepanel.net/whmcs/whmcs-corepanel-latest.zip
unzip whmcs-corepanel-latest.zip
```

A specific version is at `https://get.corepanel.net/whmcs/whmcs-corepanel-1.0.0.zip`;
`latest` always points at the current one.

### 3. Register the server in WHMCS

**System Settings → Servers → Add New Server**:

| Field | Value |
|---|---|
| Hostname | The panel's hostname, exactly as its TLS certificate spells it |
| Module | CorePanel |
| Access Hash | The token (`cpk_…`) |
| Username / Password | Leave empty — a token authenticates on its own |

Then press **Test Connection**. It reports the panel's version and refuses if the
server's edition cannot do billing automation, so a licence problem surfaces now rather
than on your first overdue invoice.

> **A panel on a non-standard port**
>
> Use the server entry's **Port** field, or write it into the hostname
> (`panel.example.com:8443`). Both work; the hostname wins if you set both.
### 4. Point a product at it

On the product, **Module Settings** has two fields:

- **Hosting package** — the name of a package in the panel, from
  [Server → Packages](https://www.corepanel.net/docs/accounts/hosting-packages). The package decides every limit
  the account gets. Left empty, accounts are created with the server defaults.
- **PHP version** — for example `8.3`. Left empty, the highest version installed on the
  server is used.

Limits live in the panel's package and nowhere else. A product that repeated them in
WHMCS would give you two places for the same number to be wrong.

## What each action does

| In WHMCS | On the server |
|---|---|
| Create | Creates the account with the product's package and PHP version |
| Suspend | [Suspends the account](https://www.corepanel.net/docs/accounts/suspension) across every plane |
| Unsuspend | Lifts it and restores what it changed |
| Terminate | Deletes the account and its data |
| Change Password | Changes the account's panel/FTP login. Its FTP *users* keep their own passwords |
| Change Package | Applies the new package's limits to the existing account |
| Usage Update | Writes each account's disk and bandwidth back into WHMCS, once a day |
| "Log in to the control panel" | Signs the customer in with a single-use link, valid for 60 seconds |

## Things worth knowing

### The panel owns the username

WHMCS generates a username when the order is placed; the module sends it to the panel and
then writes the panel's answer back onto the service. So the two sides always agree about
who the customer is, even when the panel had to adjust the name.

Names are normalised to what the panel accepts — lowercase letters and digits, not
starting with a digit, at most 16 characters — so `Shop_EU-2` becomes `shopeu2`. The
restriction is not cosmetic: an underscore would put two accounts into one MySQL
namespace, and a name that is all digits could not be told apart from an account id.

### Retrying is safe

Suspending an account that is already suspended, unsuspending one that is not, and
terminating an account that no longer exists all report **success**: the state the request
asked for is the state the server is in, and WHMCS retries failed actions. A refused token
or an unreachable server never reports success — a cancellation that never happened must
not close the service in your billing system.

### TLS is always verified

There is no option to disable certificate checking. This credential can suspend and delete
every account on the server; accepting an unverified certificate would hand that to
anything on the network able to answer for the hostname.

A server that still serves a self-signed certificate — a freshly installed or freshly
[transformed](https://www.corepanel.net/docs/cpanel-import) box — has to finish issuing a real one before WHMCS can
drive it. Registering the server under the hostname its certificate covers is usually all
it takes.

### Disk usage means the account's files

The figure reported to WHMCS is what the account's disk quota is checked against: the
files it owns. Its MySQL databases live under the database server's own user, where no
per-account quota can see them, so they are not added in — the number has to mean the same
thing as the limit printed next to it.

### When the licence lapses

Nothing about the module is disabled from our side. Each operation is checked by the
panel against the server's own entitlements, so a lapsed licence stops exactly what that
edition stops and no more: reading and reporting keep working, and a refused operation
comes back with the reason in your WHMCS activity log. See [Licensing](https://www.corepanel.net/docs/licensing).

## The compatibility layer

The module above is for a **new** server entry in WHMCS. That is fine for a new server
and wrong for an existing one: adopting it on a box you have been provisioning for years
means creating a server entry, re-pointing every product and re-linking hundreds of live
services — which nobody does to try a new panel.

So there is a second, entirely separate route. **`corepanel-whmcompat`** answers the
**WHM API on `:2087`**, the way cPanel's `whostmgrd` did, using the hostname, the IP and
the credential your WHMCS already has. After a transform your billing system keeps
talking to the same server in the same way, and there is nothing to reconfigure.

It is a different package from the panel, and you have to ask for it:

```bash
dnf install corepanel-whmcompat
corepanel whmcompat enable
```

It requires **CorePanel Business**, like the module above: `enable` is refused on any
other edition, and says so. Turning it *off* and reading its state are never refused —
a licence that lapsed must not leave you unable to close a port you opened.

That applies to the **WHM API** specifically. The same package also fixes your customers'
old `:2083` and `:2096` bookmarks, and [that part is available on every
edition](#your-customers-bookmarks) — a Personal server that came from cPanel installs
`corepanel-whmcompat` for that alone and never enables the API at all.

> **Installed is not listening**
>
> Installing it does **not** open `:2087`. The daemon starts, asks the panel whether the
> layer is switched on, and binds nothing until the answer is yes — not "binds and
> refuses": a port that was never opened has no parser and no TLS handshake behind it.
>
> The package is not part of the `corepanel` metapackage either, so a server that has no
> use for this never installs it at all. `:2087` is historically the most attacked port in
> shared hosting; opening it is a decision, never a side effect of an upgrade.
`enable` opens the port in the host firewall too, and `disable` closes it — in the
`corepanel` zone on a server that adopted [the host firewall](https://www.corepanel.net/docs/security/host-firewall),
in the distribution's default zone otherwise. The command prints which one it touched:

```
Firewall:              port 2087/tcp opened in the "public" firewall zone
```

Worth reading that line, because the two halves fail independently. A daemon listening
on `:2087` behind a firewall that drops the traffic passes `corepanel whmcompat selftest`
— which runs over loopback — and is still unreachable for your WHMCS, so the only symptom
is a server WHMCS reports as down. If the line says nothing was changed, that is because
firewalld is stopped or absent on this server, and nothing is filtering the port anyway.

If you moved the API to another port in `whmcompat.ini`, open that one yourself: the
switch manages 2087.

### The credential keeps working

Your WHMCS holds either a WHM API token or the old access hash. Import whichever it is,
once, and nothing on the WHMCS side changes:

```bash
corepanel whmcompat import-credential --user root --kind token < token.txt
```

**Only the digest is stored.** The server keeps proof of the credential, never the
credential itself — SHA-512, which is exactly what cPanel stores for a WHM API token. A
transform can therefore import your existing token straight from the cPanel install
without anybody ever learning its value, and the WHMCS that holds the secret keeps
authenticating unchanged.

Access hashes still work where they still work: a cPanel with
`allow_deprecated_accesshash` switched on — which any hosting provider still using one necessarily
has — authenticates the existing file unchanged, and so does this. Import both if you are
not sure which one is in use; they cost nothing side by side.

To see what a server accepts, and to stop accepting one:

```bash
corepanel whmcompat credentials                        # digests, labels, dates
corepanel whmcompat revoke-credential --digest a9e70ee8
```

The digest is what names a credential — a label cannot, since several can share one.
Revoking everything a user holds is `--all`, spelled out on purpose: an empty store
accepts nothing, which is how a server stops answering your billing system altogether.

### What it answers today

The compatibility layer is built against a **recorded** WHMCS session — a full service
lifecycle captured from a real WHMCS 8.11 driving a real cPanel, request and response
both — rather than against a guess at what WHMCS sends. Every row below has since been
run again the other way round: through WHMCS's own cPanel module, pointed at a CorePanel
server.

| WHMCS asks for | Status |
|---|---|
| Test Connection, server version and hostname | Answered |
| The server status panel | Answered |
| The package list on a product | Answered |
| The account list and its disk and bandwidth figures | Answered |
| Create an account | Answered |
| Suspend and unsuspend | Answered |
| Change package | Answered |
| Change password | Answered |
| Terminate | Answered |
| The nightly usage update (disk, bandwidth, overage) | Answered |
| The usage bars in your client area | Answered |
| The **"Log in to cPanel"** button | Answered, but **off until you turn it on** — see below |

An action that is not implemented is refused in the shape WHMCS expects, with a reason in
your Module Log. It never looks like the server being down.

### The login button has its own switch

The **"Log in to cPanel"** link in your client area works over this route, and it is off
until you say otherwise:

```bash
corepanel whmcompat sso enable
```

Off by default, and off on servers that were already serving `:2087` before this switch
existed — enabling the layer months ago was not consent to this.

The reason it is separate is what the capability looks like from the outside. Everything
else the layer does is **loud**: a password change stops the customer's own password
working, a suspension takes their site down, a termination shows up in three places. A
minted session is **silent** — it *is* the customer's own session, which is exactly what
makes it the right thing for that button. Most billing setups only provision and suspend
and never press it.

> **What turning it on grants**
>
> Anything holding a WHM credential for this server can then obtain a working session in
> **any** account's panel, with no password and nothing the customer would notice.
>
> The link itself is single-use, lives sixty seconds, and is authorised twice — once when
> it is minted and again when the browser arrives — so an account you suspend in between
> cannot be signed into.
While it is off, the button reports a reason in your client area that names the command
above, so an operator who turned it off by mistake finds out from their own client area
rather than from a ticket. Customers can always sign in at the panel with their account
username and password, and nothing else on this route is affected.

`corepanel whmcompat status` prints the switch in both states, on its own line, always —
it is the one thing here that leaves no trace a customer would notice, so it has to be
readable rather than inferable:

```
WHM API compatibility: enabled
Passwordless sign-in:  disabled
Legacy bookmarks:      enabled
```

The [module at the top of this page](#setting-it-up) is not affected by any of this: it
reaches the same feature through an API token you issued, over the port the panel already
owns, and its login button works whatever this switch says.

### Your customers' bookmarks

The rest of the old cPanel port set is a separate problem with a separate switch, and it
is the one your customers notice first.

On a cPanel server, `:2083` is *the* URL every customer has saved and `:2096` is how they
reach their webmail. The day you transform the server those become a refused connection —
no page, no explanation, no clue where their panel went — for everybody at once.

```bash
corepanel whmcompat redirects enable
```

![What answers the old cPanel ports. On the left, three kinds of caller: a customer's old panel bookmark on https://sudominio.com:2083, their webmail bookmark on :2096, and anything that is not a browser — a /json-api/ path or a request carrying an Authorization header. In the middle, corepanel-whmcompat listening on 2082, 2083 and 2086 and on 2095 and 2096, asking one question of every request: is this a browser? A browser gets 302 Found; anything else gets an empty 404. On the right, where each lands: the panel at https://panel.suempresa.net/, their own webmail at https://webmail.sudominio.com/ only if this server serves that domain, and nowhere at all for the API client, with no Location header. The redirect target is built from the server's own configuration, never from the request.](https://www.corepanel.net/_astro/whmcompat-legacy-ports.xPOExHo_.svg)

With that on, this server answers those ports and sends the browser where the thing lives
now:

| Port | TLS | A browser is sent to |
|------|:---:|----------------------|
| 2082 | no  | the panel |
| 2083 | yes | the panel |
| 2086 | no  | the panel |
| 2095 | no  | `webmail.<domain>`, for the domain it asked for |
| 2096 | yes | `webmail.<domain>`, for the domain it asked for |

**This one is available on every edition**, unlike the WHM API above. It is not part of
"WHMCS integration": what it serves is a redirect to your own panel, it holds no
credential and it reads no parameter. A Personal server that came from a transform has
exactly the same broken bookmarks as a Business one, so it gets the same fix.

It is off by default all the same, including on servers already serving `:2087` — five
ports on an internet-facing box is a decision you make, not something an upgrade does for
you. The switch opens and closes them in the host firewall along with everything else.

> **Your customers will see a certificate warning on :2083 and :2096, once**
>
> Those two are TLS, and this server answers them with **its own** host certificate — so a
> browser going to `https://sudominio.com:2083` warns about the name before it ever sees the
> redirect. That is exactly what cPanel did too: the same URL warned there unless the domain
> had a certificate of its own.
>
> It is one click, and then they land on the panel over a properly trusted name. A customer
> who types `sudominio.com:2083` without `https://` reaches the plaintext port instead and
> sees no warning at all — which is what most people actually do.
>
> The alternative was giving an internet-facing daemon read access to every domain's private
> key, to avoid one click. That is not a trade worth making.
What these ports will not do, because getting this wrong is worse than not doing it:

- **They never redirect something that is not a browser.** A request on a `/json-api/`
  path, or one carrying an `Authorization` header, is never given a redirect — an
  automated call that follows a 302 to a login page reports whatever it finds there as
  success. On `:2086`, cPanel's *cleartext* WHM API port, an API client gets a readable
  refusal pointing at `https://<hostname>:2087` instead; the other four answer nothing at
  all, as they did on cPanel.
- **They never send a browser to an address from the request.** The webmail target is
  built from the `Host` header only after the panel confirms this server serves that
  domain. A request naming somebody else's domain lands on your panel. Without that check
  these would be an open redirect on the most-scanned ports on the server.
- **They never send a permanent redirect.** A 301 would be cached in the browsers of
  people who are not your customers, outliving any later change you make.

`corepanel whmcompat status` names which of the five are actually bound. Fewer than five
means something else on the server is holding the rest — worth checking on a freshly
transformed box, where a leftover from the old panel is the usual cause.

### Where it deliberately differs from cPanel

Five places, and they are all in the direction of not lying to you:

- **Terminating an account that is already gone reports success.** cPanel reports an
  error. WHMCS retries what fails, so copying cPanel would leave a cancelled service
  stuck in a retry loop over a termination that had already worked. Suspending an
  already-suspended account also succeeds — that one matches cPanel.
- **`keepdns` is not honoured.** Deleting an account always takes its DNS zones with it.
  The deletion still goes ahead, because refusing it would leave a customer hosted after
  they cancelled, and the answer says the zone went too.
- **A password change moves fewer credentials.** On cPanel, `passwd` rotates the system,
  FTP, mail and MySQL passwords together. Here it changes the account's panel login and
  its Linux user — so SSH and SFTP — and deliberately leaves FTP users, mailboxes and
  database users alone: those are separate credentials from the moment they are created.
  The answer reports exactly what changed rather than cPanel's four.
- **A downgrade below current usage is refused**, with a reason naming each limit and
  what the account is actually using. cPanel would apply it. A quota silently set below
  current usage is how an account ends up unable to write to its own files with nothing
  to point at.
- **Bandwidth counts web traffic, and only the current month.** The figure your usage
  update collects is the bytes CorePanel's web server served for that account. Mail, FTP
  and anything an application opens for itself are not in it yet, and neither is the
  protocol overhead underneath. **Expect your numbers to drop after a migration** — they
  are lower than cPanel's, not different in units. There is also no history: a request
  for a past month is refused rather than answered with this month's figures under last
  month's label, so run your usage update on its normal schedule and it will never
  notice.

Check this side before touching WHMCS:

```bash
corepanel whmcompat selftest
```

That drives a real request over loopback — the port, TLS, the credential, the response
envelope and the call into the panel, with nothing stubbed. When it passes, press **Test
Connection** in WHMCS: only WHMCS proves WHMCS.

Full command reference: [`corepanel whmcompat`](https://www.corepanel.net/docs/cli#corepanel-whmcompat).

## What is not supported

- **Third-party WHMCS addons.** cPanel Extended and similar addons drive hundreds of
  per-user cPanel API calls. Neither the module nor the compatibility layer implements a
  per-user cPanel API, and there are no plans to.
- **Creating resellers.** WHMCS cannot make one: `setupreseller`, the per-reseller ACL
  list and setting a reseller's limits are not implemented on either route. Resellers are
  created in the panel or from the CLI.

  Provisioning **on behalf of** an existing reseller does work, on the compatibility
  layer: `createacct` honours WHM's `owner=`, so a product configured for a reseller
  creates the account under that reseller, and it counts against
  [that reseller's own ceilings](https://www.corepanel.net/docs/accounts/quotas#limits-above-the-account-the-resellers-totals) —
  a full reseller is refused, with the wall named, exactly as the panel's own button is
  refused. An `owner` naming nobody on this server is refused rather than quietly
  reassigned to you, and the refusal lists the resellers that do exist.
- **The admin's "Log in to WHM" button.** That is a different path from the customer's:
  it asks for a session as `root`, which is not an account on a CorePanel server, and it
  is answered as the missing user it is. The *customer-facing* login button does work —
  on the module always, and on the compatibility layer once you
  [turn it on](#the-login-button-has-its-own-switch).

## Troubleshooting

Every call the module makes is recorded in **Utilities → Logs → Module Log** in WHMCS,
with the request, the answer and the reason for any refusal. The panel's own wording is
passed through unchanged, so an error there says what the panel would have said.

That log is **off by default** — the page has an *Enable Debug Logging* button. Turn it on
before reproducing a problem, and off again afterwards: it records every module on the
system, not only this one.

| What you see | What it means |
|---|---|
| `CorePanel rejected the API token` | The Access Hash is wrong, or the token was revoked in the panel |
| `requires the "provisioning" scope` | The token was created without the scope this action needs. Scopes are fixed for a token's life — create a new one |
| `The panel's TLS certificate could not be verified` | The server is registered under a name the certificate does not cover, or the panel has not issued one yet |
| `No CorePanel account matches username "…"` | The account was renamed or removed in the panel; the service in WHMCS points at a name the server does not have |
| `No hosting package named "…" exists` | The product's **Hosting package** does not match a package in the panel. The message lists the ones that do exist |
| `licence does not cover the operation` | The server's edition does not include this action. See [Licensing](https://www.corepanel.net/docs/licensing) |
