Every WHM server that talks to a billing platform, a monitoring agent, or a CI job is authenticating something on every call. If that something is the root password — or a session cookie scraped from a logged-in browser — you have a credential that can do anything WHM can do, sitting in a config file, an environment variable, or a CI secret store. WHM API tokens are the fix. They are scoped, revocable, attributable, and they cost nothing to issue.
This guide covers how to create a token, scope it down to the minimum ACLs it needs,
test it against whmapi1, and rotate it without breaking the systems that depend on it.
Why tokens, not passwords or session cookies
Three concrete reasons to stop authenticating WHM automation with the root password:
- Rotation breaks everything at once. Change the root password and every script that embedded it stops working until you've found and updated each one. A token rotation is one-at-a-time and lets you cut over without downtime.
- Tokens have ACLs; the root password doesn't. A billing module that only needs to
create accounts and suspend them should not be able to read
/etc/shadowover WHM'sfetchsslinfocalls or run arbitrary shell viacpanel_exec. - Audit trail. Every API call signed with a token is logged with the token name in
/usr/local/cpanel/logs/api_tokens_log. Root-password auth shows up as "root", and you cannot tell which automation it came from.
Session cookies are worse still — they expire, can't be CIDR-restricted, and grant the full ACL set of the user they belong to.
Step 1 — Create a token in the UI
In WHM → Development → Manage API Tokens → Generate Token:
- Name — descriptive, no spaces.
blesta-prod,prometheus-exporter,nightly-backup-rotation. The token name shows up in audit logs; future you will thank present you for naming things specifically. - Expiration — set one. WHM defaults to "never expires," which is the wrong default for anything other than infrastructure tokens you actively monitor. Six or twelve months is reasonable. The UI will not warn you when a token is about to expire, so put the date in your calendar when you create it.
- ACL — start with None, then grant only what the caller actually needs. The default suggestions ("All Features") are the equivalent of granting root.
Click Save. WHM displays the token once — copy it now. If you close the dialog without saving the token somewhere durable, you have to delete it and generate a new one.
The token format is a 32-character string. WHM stores only a hash; nothing on the server can reveal the plaintext after the dialog closes.
Step 2 — Generate from the CLI when you're scripting onboarding
For provisioning automation, the UI is the wrong tool. Use whmapi1 directly:
whmapi1 api_token_create token_name=blesta-prod \
acl-1=create-acct acl-2=kill-acct acl-3=suspend-acct \
acl-4=list-accts acl-5=show-bandwidth \
expires_at=1797811200
expires_at is a Unix timestamp. Generate one with date -d "+1 year" +%s on Linux or
date -v+1y +%s on macOS. The full list of ACL names is in /var/cpanel/acllists/ —
each file is a feature group, and the filename is the ACL string you pass to
api_token_create.
The response includes the token value at data.token. Capture it from the JSON, write it
to your secret store, and discard the shell history line that contained it:
TOKEN=$(whmapi1 --output=jsonpretty api_token_create token_name=blesta-prod \
acl-1=create-acct acl-2=suspend-acct | jq -r '.data.token')
# write to your secret store, e.g.
op item create --category=password --title="WHM blesta-prod" password="$TOKEN"
Step 3 — Scope ACLs to the actual workload
The five most common automation profiles, and the minimum ACLs each needs:
| Workload | Required ACLs |
|---|---|
| Billing module creating/suspending accounts (Blesta, WHMCS) | create-acct, kill-acct, suspend-acct, upgrade-account, list-accts, edit-account |
| Monitoring (uptime, load, disk) | basic-system-info, show-bandwidth, list-accts |
| DNS automation | add-dns, edit-dns, kill-dns, park-dns |
| Backup orchestration | list-accts, restore-base-understanding, backup |
| SSL automation | manage-ssl, ssl, ssl-gencrt |
Do not grant all unless the caller is genuinely a root replacement (and if it is,
rotate it monthly and CIDR-restrict it). Avoid cpanel_api — it lets the token act as
any cPanel user on the server, which is essentially root via a different door.
If you discover a token needs an additional ACL later, update it in place rather than re-issuing:
whmapi1 api_token_update token_name=blesta-prod new-acl-1=park-dns
The token value does not change; only the ACL set does.
Step 4 — Test the token before handing it to automation
Authenticate with the WHM USERNAME:TOKEN header format. Use root as the username for
tokens issued under the root account, or the reseller's username for reseller-issued
tokens:
curl -sk \
-H "Authorization: whm root:THE_TOKEN_VALUE_HERE" \
"https://server1.example.com:2087/json-api/listaccts?api.version=1" | jq '.metadata'
A successful call returns metadata.result: 1. A 403 means the ACL is missing; a 401
means the token name or value is wrong; a 503 means cpsrvd is restarting (give it 30
seconds).
For deeper testing, hit a write endpoint in a controlled way. The cleanest "this token
actually works" check is applist, which returns the list of API functions the token can
call:
curl -sk -H "Authorization: whm root:$TOKEN" \
"https://server1.example.com:2087/json-api/applist?api.version=1" | jq '.app | length'
If the number is shorter than you expect, you missed an ACL.
Step 5 — Lock down where the token can be used
WHM does not natively bind tokens to source IPs — that's a gap. The two ways to close it:
- Firewall the WHM port (2087) at the host. Limit inbound
tcp/2087to the CIDRs that legitimately call WHM (your office, your billing host, your monitoring system). Pair with a working cPHulk configuration so brute-force noise doesn't drown the log. - Run automation through a bastion that holds the token and exposes a narrower internal API. This is overkill for a single server, but if you have a fleet of 50 cPanel hosts, one signing service with per-host tokens beats 50 secrets in every CI job.
Step 6 — List, revoke, and rotate
List active tokens with their ACL sets and last-used timestamp:
whmapi1 api_token_list_tokens
Revoke a token by name. The change is immediate; in-flight requests using that token will get a 401 on the next call:
whmapi1 api_token_revoke token_name=blesta-prod
Rotation is "create new, switch callers, revoke old." Never edit the token value (you can't), and never reuse a token name within seconds of revoking it — the audit log will conflate the two.
A rotation script for a single caller looks like:
NEW=$(whmapi1 --output=jsonpretty api_token_create token_name=blesta-prod-2026q3 \
acl-1=create-acct acl-2=suspend-acct | jq -r '.data.token')
# Update the caller (Blesta module config, env var, secret manager) with $NEW
# Verify the next scheduled billing run succeeds
# Then revoke the old one
whmapi1 api_token_revoke token_name=blesta-prod
Build this into the same playbook you use for WHM transfer operations — credential rotation belongs next to credential creation, not in a separate runbook that drifts.
Next steps
- Tighten brute-force defaults so a leaked token doesn't sit alongside an open SSH door: Tune WHM cPHulk.
- If the automation is a reseller-facing tool, scope at the user layer instead: Set up WHM reseller accounts with the right ACLs.
- Sizing a new automation-heavy server? The cPanel license tiers cap account counts independently of how you authenticate to WHM, and your cPanel license is what gates the API surface at all.