Ahosting Logo
Knowledge Base

How to Use the WHM API and Command Line

WHM's API lets you do from a script anything you can do in the panel: create accounts, change packages, suspend, list what you have. For a reseller this matters in three situations, onboarding many accounts at once, connecting your own system to hosting, and generating reports the panel does not offer.

It is also what billing software uses underneath. Worth knowing, because if you find yourself scripting account creation regularly, you are rebuilding something that already exists.

Two ways in

From the command line over SSH, using the whmapi1 wrapper. Simplest, and the right choice for one-off jobs and shell scripts.

Over HTTPS from anywhere, authenticating with an API token. This is how an external application talks to the server.

Both hit the same API and take the same parameters, so a call worked out on the command line translates directly into a script.

Create a token, do not use your password

In WHM, open Development then Manage API Tokens and create one.

The token is displayed once. Copy it immediately. There is no way to retrieve it later, only to delete it and make another.

Restrict what it can do. A token that only needs to list accounts should not be able to terminate them. If a token leaks, its privileges are exactly the damage available, and a read-only token in a log file is a very different problem from one that can delete accounts.

Use one token per integration, named for its purpose. When something needs revoking you then revoke that one thing rather than breaking every script you have.

What the API is for, and what the panel is still better atUse the API· creating many accounts from a list· reports the panel does not offer· anything you will do again next month· work that must be repeatable exactlyUse the panel· one-off changes· anything you want to see before confirming· work where the screen shows context you would have to query forThe rule worth keepingAn API call does exactly what you typed, including the part you typed wrongly. Try it againstone account first.

From the command line

Connect over SSH and call functions directly. List every account you host:

whmapi1 listaccts

Create one:

whmapi1 createacct username=clienta domain=clienta.com plan=web-5gb

Suspend and unsuspend:

whmapi1 suspendacct user=clienta reason='invoice 1042 unpaid'
whmapi1 unsuspendacct user=clienta

Change an account's package:

whmapi1 changepackage user=clienta pkg=web-20gb

Output is YAML by default. For anything you intend to parse, ask for JSON:

whmapi1 --output=jsonpretty listaccts

Over HTTPS

The same calls, authenticated with the token:

curl -H "Authorization: whm reselleruser:YOUR_TOKEN_HERE" \
 "https://server.example.com:2087/json-api/listaccts?api.version=1"

Note port 2087 and https. Sending a token over plain HTTP hands it to anyone on the network path.

Creating an account is the same shape with parameters in the query string:

curl -H "Authorization: whm reselleruser:YOUR_TOKEN_HERE" \
 "https://server.example.com:2087/json-api/createacct?api.version=1&username=clientb&domain=clientb.com&plan=web-5gb"

Bulk creation with different packages

This is the case the web form cannot handle: many accounts where each needs its own package.

while IFS=, read -r user domain plan; do
 whmapi1 createacct username="$user" domain="$domain" plan="$plan"
 sleep 2
done < accounts.csv

With accounts.csv holding one account per line:

clienta,clienta.com,web-5gb
clientb,clientb.net,web-20gb
clientc,clientc.org,web-5gb

The sleep 2 is deliberate. Account creation is heavy work, and firing forty at once makes the server slow for everyone, including the clients you already have.

Passwords are generated when omitted, and they appear in the API output: capture that output to a file, or you have forty accounts and no credentials.

Reading what you have

Useful for reports the panel does not produce. Disk usage per account, sorted:

whmapi1 --output=json listaccts | \
 python3 -c "import json,sys; d=json.load(sys.stdin)['data']['acct']; \
 [print(a['user'], a['diskused'], a['disklimit']) for a in d]"

Every suspended account with its reason:

whmapi1 --output=json listaccts | \
 python3 -c "import json,sys; d=json.load(sys.stdin)['data']['acct']; \
 [print(a['user'], a.get('suspendreason','')) for a in d if a['suspended']]"

That second one is worth running periodically. Suspended accounts still occupy slots on your plan, and a list of what is suspended and why is exactly the review that stops dead accounts accumulating.

Reading the response

Every call returns a result field. 1 means success, 0 means failure, and the reason field explains why.

Check it. A script that fires calls without reading responses fails silently, and you discover it when a client asks where their account is:

result=$(whmapi1 --output=json createacct username=x domain=x.com plan=web-5gb)
echo "$result" | grep -q '"result":1' || echo "FAILED: $result"

What you cannot do

A reseller token has reseller privileges, not root. Calls that restart services, change server-wide settings or touch accounts belonging to other resellers are refused.

That refusal is correct and not a configuration problem to work around. If a task genuinely requires root, a reseller plan is not the right tier for it.

When not to script this

Once billing software is connected to WHM, account creation should go through billing rather than through your own scripts, billing calls this same API and adds invoicing, suspension and client notification on top.

An account created by a script never gets invoiced, works perfectly, and is discovered a year later. Integrating billing and automation goes over the connection.

Keep scripting for what billing does not do: reports, bulk migrations, and one-off maintenance across many accounts at once.

Handle the response rather than the exit code

Requests to the interface succeed at the transport level and report failures inside the body, so a script checking only the status code treats errors as successes.

curl -sH "Authorization: whm root:TOKEN" \
  'https://example.com:2087/json-api/listaccts?api.version=1' | head -c 300
echo

The response carries a result field and a reason. Read both before acting on the data, since a request that was refused returns a valid structure containing no accounts rather than an error.

In a script, check that field explicitly. The difference between an empty list and a refused request is invisible otherwise, and a bulk job that silently did nothing is worse than one that failed loudly.

Rate limit your own scripts

A loop making requests as fast as it can will affect the server it is querying, which on a machine serving customers is a self inflicted problem.

for u in $(cat users.txt); do
  curl -sH "Authorization: whm root:TOKEN" \
    "https://example.com:2087/json-api/accountsummary?api.version=1&user=$u" > "out/$u.json"
  sleep 1
done

A short pause between requests costs nothing on a job of a few hundred and prevents the version that generates a support ticket from a customer.

Where a job is genuinely large, run it at a quiet hour and write the output to files rather than holding everything in memory. That also makes an interrupted run resumable.

Keep the token out of the command

A credential typed into a command is recorded in the shell history and visible in the process list to anybody on the machine.

chmod 600 ~/.whmtoken
TOKEN=$(cat ~/.whmtoken)
curl -sH "Authorization: whm root:$TOKEN" 'https://example.com:2087/json-api/version?api.version=1'

Reading it from a file with restricted permissions removes both exposures, and the file can be replaced without editing every script that uses it.

Issue a separate token per script rather than reusing one. When something has to be revoked, only that job stops rather than everything you have automated. Using API tokens covers issuing them.

Watch these steps on screen 1 clip · 0:28

Recorded on a real panel, no narration, captions on screen. Opens here without leaving the page and without an account. Every name, address and figure shown is made up for the recording.