A testable WHMCS API designed to run standalone or as part of a Laravel application.
Requirements:
- PHP 8.4
- WHMCS
composer require eugenefvdm/whmcs-apiPublish the configuration file:
php artisan vendor:publish --provider="Eugenefvdm\Whmcs\WhmcsServiceProvider" <?php
return [
'url' => env('WHMCS_URL'),
'admin_url' => env('WHMCS_ADMIN_URL'),
'api_identifier' => env('WHMCS_API_IDENTIFIER'),
'api_secret' => env('WHMCS_API_SECRET'),
'debug' => env('WHMCS_DEBUG'),
'limitnum' => env('WHMCS_LIMITNUM', 10000),
'default_payment_method' => env('WHMCS_DEFAULT_PAYMENT_METHOD', 'mailin'),
'default_reg_period' => env('WHMCS_DEFAULT_REG_PERIOD', 1),
'verify_ssl' => env('WHMCS_VERIFY_SSL', true),
];WHMCS_URL should be without the trailing slash.
For local installs without a valid TLS certificate (e.g. https://whmcs.test), set WHMCS_VERIFY_SSL=false.
WHMCS_DEFAULT_REG_PERIOD is optional; used by addDomainRegistrationOrder() when no period is passed.
Allow the IP address of the system connecting to WHMCS:
Systems Settings => General => Security => API IP Access Restriction
Create API permissions:
System Settings => API credentials => Generate New API Credential
- addclient
- addorder
- getclientsdetails
- getclientsdomains
- getorders
- getorderstatuses
- custom list of API calls you are developing
test('it can create a new client in WHMCS', function () {
$customfields = base64_encode(
serialize(
[
8 => '50-100',
]
)
);
$client = [
'firstname' => $this->faker->firstName,
'lastname' => $this->faker->lastName,
'email' => $this->faker->email,
'address1' => $this->faker->numberBetween(1, 100).' '.$this->faker->streetName,
'address2' => $this->faker->secondaryAddress,
'city' => $this->faker->city,
'state' => $this->faker->state,
'postcode' => $this->faker->postcode,
'country' => config('whmcs.default_country'),
'phonenumber' => $this->faker->phoneNumber,
'password2' => bin2hex(random_bytes(8)),
'customfields' => $customfields,
];
$result = Whmcs::addClient($client);
expect($result)->toHaveKey('result')
->and($result['result'] == 'success');
})->only();WHMCS removed the ability to add custom API calls, but there is a hack to get it working again.
An example of a custom API call would be to get a client by their phone number. Let's call this getclientbyphonenumber. At least two steps are required.
- Create a custom API call
- Inject the permission into the database
Save the example code below here:
includes/api/getclientbyphoneumber.php
<?php
use WHMCS\Database\Capsule;
if (!defined("WHMCS"))
die("This file cannot be accessed directly");
try {
$client = Capsule::table('tblclients')
->where("phonenumber", $_REQUEST['phonenumber'])
->first();
if ($client) {
$apiresults = [
"result" => "success",
"message" => "ok",
'clientid' => $client->id,
];
} else {
$apiresults = [
"result" => "error",
"message" => "not found",
];
}
} catch (Exception $e) {
$apiresults = ["result" => "error", "message" => $e->getMessage()];
}Next to use the custom API call getclientbyphonenumber you need to manually update tblapi_roles and add it there.
Also remember to update it every time again you make a changes in the UI because the UI will overwrite the custom API call.
In the example below, the JSON in the database has been updated to include the new API call getclientbyphonenumber.
{"addclient":1,"getclientsdetails":1,"getclientbyphonenumber":1}If you haven't added the PHP file yet, you'll get API Function Not Found.
<?php
use Eugenefvdm\Whmcs\Whmcs;
require 'vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$api = Whmcs::connect([
'url' => env('WHMCS_URL'),
'api_identifier' => env('WHMCS_API_IDENTIFIER'),
'api_secret' => env('WHMCS_API_SECRET'),
]);
$result = $api->getClients();Use the Whmcs facade after publishing config:
use Eugenefvdm\Whmcs\Facades\Whmcs;
$result = Whmcs::getClientsDetails(['clientid' => 1]);See CHANGELOG for more information on what has changed recently.
Prefer Whmcs::connect() for an isolated client instance — safe for per-tenant credentials or multiple servers in the same request:
use Eugenefvdm\Whmcs\Whmcs;
$api = Whmcs::connect([
'url' => env('WHMCS_URL2'),
'api_identifier' => env('WHMCS_API_IDENTIFIER2'),
'api_secret' => env('WHMCS_API_SECRET2'),
]);
$result = $api->getClientsDetails(['clientid' => 1]);setServer() mutates the current instance and is deprecated. The Laravel facade resolves a shared binding, so use connect() when switching servers.
List orders for a client:
$orders = Whmcs::getOrders(['userid' => $clientId]);
$items = Whmcs::ordersList($orders);ordersList() normalizes WHMCS's inconsistent single-item vs array responses via WhmcsResponse::list().
For Filament custom-data tables, use ordersPaginator() — returns a LengthAwarePaginator keyed by order id:
$paginator = Whmcs::ordersPaginator([
'userid' => $clientId,
'page' => 1,
'limitnum' => 25,
]);Place a domain registration order (wraps AddOrder):
Whmcs::addDomainRegistrationOrder(
clientId: $clientId,
domain: 'example.com',
paymentMethod: 'mailin',
regPeriod: 1, // optional; defaults to config('whmcs.default_reg_period')
);Fetch order statuses for filter options:
$statuses = Whmcs::getOrderStatuses();Pagination defaults (limitstart, limitnum) are applied only on list endpoints such as getOrders() and getClients() — not on addOrder() or other write calls.
Whmcs::changePlan($serviceId);Applies a new package to the service via ModuleChangePackage. If the package is linked to a provisioning module, WHMCS will call it.
API errors throw WhmcsApiException with the WHMCS action name and parsed response:
use Eugenefvdm\Whmcs\WhmcsApiException;
try {
Whmcs::getOrders(['userid' => $clientId]);
} catch (WhmcsApiException $e) {
$e->action; // e.g. 'GetOrders'
$e->response; // full WHMCS JSON body
$e->getMessage();
}WhmcsResponse helpers are available for any nested WHMCS collection:
use Eugenefvdm\Whmcs\WhmcsResponse;
$clients = WhmcsResponse::list($response, 'clients', 'client');
$total = WhmcsResponse::total($response);Before testing, please ensure you have copied over the .env.example file to .env as the tests use dotenv.
cp .env.example .envSet WHMCS_DEBUG=true in your .env file to enable debugging. When enabled, request and response payloads are sent to Spatie Ray if it is installed.
./vendor/bin/pest
To test custom API actions, use a script such as the following:
sh .scp updateclientaddon.php;./vendor/bin/pest
The .scp file should have a copy command, e.g.:
#!/bin/bash
echo "Present Working Directory:"
pwd
echo "Copying $1 to WHMCS install directory"
cp includes/api/$1 ../whmcs/includes/api
echo "Done."
ls -la ../whmcs/includes/api/$1API actions are difficult to troubleshoot if you don't observe the server log file. Only some exceptions, e.g., model problems will be caught by the Try Catch block. So help tail your server log file. For example, if you're using Laravel's Valet's Nginx server, do this:
tail -f ~/.valet/Log/nginx-error.log
If you get Invalid IP 127.0.0.1 that means you haven't allowed WHMCS API access from localhost.
Navigate here: https://whmcs.test/admin/configgeneral.php and make sure you add 127.0.0.1 to API IP Access Restriction.
If you get Invalid or missing credentials that means you haven't added API roles and API credentials. Both are required before you can test. Also be sure to add them to your .env file:
WHMCS_API_IDENTIFIER=
WHMCS_API_SECRET=
If you get Invalid Permissions: API action "addclient" is not allowed that means,
although you've added API roles and credentials, your roles are not set up properly for the API call.
Revisit roles and the requisite subsection and see where you have to click the checkbox to allow this API call.
The storage folder has examples API responses, also used for caching during tests.
For local editing, add this to composer.json:
"repositories" : [
{
"type": "path",
"url": "../whmcs-api"
}
],
Then in require section:
"eugenefvdm/whmcs-api": "dev-main",
Then do this to symlink:
composer require eugenefvdm/whmcs-api:dev-mainMIT
hello (at) eugenefvdm.com
https://eugenefvdm.com
+27 82 309–6710
I am a Laravel, hosting, and WHMCS specialist.
Contact me any time for assistance.