A custom WHMCS provisioning module is a PHP file at modules/servers/yourmodule/yourmodule.php whose functions WHMCS calls to create, suspend, unsuspend and terminate a service on another system. Each function is named yourmodule_FunctionName and receives a $params array. The core functions must return the word success or an error message. Only _ConfigOptions is required.
I have built 200+ custom WHMCS modules. I checked every function name, parameter and menu path below against the official WHMCS developer documentation and sample module again on 30 October 2026.
What does a WHMCS provisioning module do?
A WHMCS provisioning module, also called a server or product module, carries out the lifecycle of a service on a remote system when billing events happen. The official introduction names the events: payment of a new order, an item becoming overdue, payment of an overdue invoice, and a cancellation request.
That is a different job from the two neighboring tools. A hook reacts to an event with a side effect, which I cover in my WHMCS hooks guide. The API lets other software call WHMCS, covered in my WHMCS API guide. A provisioning module is the code that talks to the remote system: a control panel, a cloud platform, a license server or your own application.
Where do the module files go, and how must they be named?
The files go in a folder of their own under /modules/servers/, and the main PHP file carries the same name as the folder. The getting started page of the documentation gives the naming rule: one word, only lowercase letters and numbers, starting with a letter, and unique. So acmepanel follows the rule, and acme_panel or AcmePanel does not.
The official sample module on GitHub recommends this layout:
modules/servers/acmepanel/
acmepanel.php the module functions
hooks.php optional hooks that ship with the module
lib/ your own classes, such as an API client
templates/ Smarty templates for client area output
logo.png
Every function in acmepanel.php starts with the file name and an underscore, for example acmepanel_CreateAccount. If the remote system cannot do something, leave that function out, as the sample module advises.
Which functions can a WHMCS provisioning module contain?
The documentation lists seven core functions (Create, Suspend, Unsuspend, Terminate, Renew, ChangePassword and ChangePackage) plus setup and optional functions. This table has the eleven I use most, with the suffix that follows your module name.
| Function | When WHMCS calls it | Result of a success |
|---|---|---|
_MetaData | Not tied to a service. It describes the module. | Returns an array with keys such as DisplayName, APIVersion (use 1.1) and RequiresServer. |
_ConfigOptions | Not tied to a service. It defines the product's module settings. | Returns up to 24 per-product fields: text, password, yes/no, dropdown, radio or textarea. |
_CreateAccount | When a new product provisions: at checkout or on payment, or when an admin runs it. | Status becomes Active and the Product Welcome Email is sent. |
_SuspendAccount | By the cron when a product becomes overdue, or by an admin. | Status becomes Suspended. |
_UnsuspendAccount | On payment of an overdue invoice for the product. | Status becomes Active. |
_TerminateAccount | By the cron for long-overdue products when termination is enabled in the automation settings, or by an admin. | Status becomes Terminated. |
_Renew | Each time a renewal invoice for the product is paid. | No status change is documented. |
_ChangePassword | When a client or admin asks for a password change on an active product. | The password is updated in the WHMCS database. |
_ChangePackage | When an upgrade or downgrade order is paid, for products and for configurable options. | No status change is documented. |
_TestConnection | When staff click Test Connection while adding or editing a server. | Returns an array with success and error. |
_ServiceSingleSignOn | When a client or admin asks to log in to the service's control panel. | Returns an array with success and redirectTo, or errorMsg. |
The full list is on the supported functions page. It also covers _ClientArea and _AdminCustomButtonArray, _UsageUpdate for the daily disk and bandwidth import, _MetricProvider for usage billing metrics, and _CustomActions, which puts a button on the Active Products/Services panel of the client area home page and sends the customer to a URL.
What does WHMCS pass in the $params array?
WHMCS passes the same $params array to every module function except _ConfigOptions, and it describes the service, the product settings, the client and the server. The keys I use in almost every module are these.
- The service:
serviceid,userid,pid,domain,usernameandpassword. - The product's module settings:
configoption1toconfigoption24, numbered in the order you declare them in_ConfigOptions. - What the customer chose:
customfieldsandconfigoptions, both keyed by name, for example$params['configoptions']['Disk Space']. - The client:
clientsdetails, an array with the name, email, address and country. - The server:
serverhostname,serverip,serverusername,serverpassword,serveraccesshash,serversecureandserverport.
Do not confuse configoptionN with configoptions. The first holds the settings staff entered on the product. The second holds the product's configurable options, which the customer picks when ordering. The module parameters page has every key.
Most remote systems return an ID of their own. Store it with service properties: $params['model']->serviceProperties->save(['Remote ID' => $id]) writes to a custom field with that name and creates the field if it is missing.
What does a minimal _CreateAccount function look like?
A minimal module needs the direct-access guard, _MetaData, _ConfigOptions and one core function that returns success or an error string. acmepanel_request() below stands for your own HTTP client, which throws an exception when the panel rejects the request.
<?php
if (!defined('WHMCS')) {
die('This file cannot be accessed directly');
}
function acmepanel_MetaData()
{
return ['DisplayName' => 'Acme Panel', 'APIVersion' => '1.1', 'RequiresServer' => true];
}
function acmepanel_ConfigOptions()
{
return [
'plan' => ['FriendlyName' => 'Plan', 'Type' => 'dropdown', 'Options' => 'starter,business'],
];
}
function acmepanel_CreateAccount(array $params)
{
$request = [
'username' => $params['username'],
'password' => $params['password'],
'email' => $params['clientsdetails']['email'],
'plan' => $params['configoption1'],
];
$hide = [$params['password'], $params['serverpassword']];
try {
$response = acmepanel_request($params, '/accounts', $request);
logModuleCall('acmepanel', __FUNCTION__, $request, $response, null, $hide);
} catch (Exception $e) {
logModuleCall('acmepanel', __FUNCTION__, $request, $e->getMessage(), null, $hide);
return $e->getMessage();
}
return 'success';
}
The return value is the contract. The documentation says the function must return the word success, and that anything else should be an error message a person can understand, because it is shown to staff. Errors from _ChangePassword are also shown to the client, so never return a raw API reply that contains credentials.
How do you make a module's settings easier to fill in?
Mark the everyday fields with 'SimpleMode' => true and let a loader function fetch the rest from the remote system. Both keys sit on the field definitions inside _ConfigOptions, beside FriendlyName and Type, and both cut the number of values staff have to type correctly when they build a product.
Simple Configuration Mode decides which module settings appear by default. Fields carrying SimpleMode show straight away, and the rest stay hidden until the user switches to Advanced Mode. The documentation uses the cPanel module to show the difference.
A loader function fills a field from the remote API instead of a hard-coded dropdown. Add a Loader key naming your function, then have that function return an array of key and label pairs, where the key is the value your module expects to receive. Plan names are the usual case: staff pick from the panel's live list instead of typing a name that has to match it exactly. Here is the Plan field from the example above, rewritten so the panel supplies the list.
function acmepanel_ConfigOptions()
{
return [
'plan' => [
'FriendlyName' => 'Plan',
'Type' => 'text',
'SimpleMode' => true,
'Loader' => 'acmepanel_PlanList',
],
];
}
function acmepanel_PlanList($params)
{
// ['starter' => 'Starter', 'business' => 'Business'], read from the panel
}
If the call to the remote API fails, the documentation says to throw an exception rather than return an empty list. WHMCS shows the exception message to the person editing the product, so write it for them: say which system did not answer, not which line of code threw.
How do you log and debug module calls?
Call logModuleCall() around every request to the remote system, then read the Module Log at Configuration > System Logs. Its arguments are the module name, the action, the request, the response, the processed data and an array of strings to scrub from the entry, which is where passwords and tokens go.
Two things surprise people. The Module Log records nothing until you set Module Logging to On, and the logs documentation says the admin role needs the View Module Debug Log permission. And when an automated action fails, it appears at Utilities > Module Queue, where the documentation is blunt: WHMCS will not retry it automatically. Someone has to click Retry. Retry All reruns everything still open in the list, and Mark Resolved takes an item off the list without running it, which is what you want once you have fixed that account by hand.
How do you test a provisioning module before customers use it?
Test it on a staging copy of WHMCS against a test account on the remote system, in this order.
- Add the server at Configuration > System Settings > Servers and click Test Connection, which runs your
_TestConnectionfunction. - Create a product at Configuration > System Settings > Products/Services. On the Module Settings tab, choose your module under Module Name and select "Do not automatically setup this product".
- Order the product for a test client. Run the create, suspend, unsuspend and terminate module commands from the service in the client's profile, and compare each Module Log entry with the remote system.
- Run every command a second time. Staff will do that in real life, so a create on an existing account and a terminate on a missing one must not cause damage.
- Switch the product to automatic setup on first payment, pay a test invoice, and confirm that the welcome email arrives only after the account works.
When should you not build a custom provisioning module?
Do not build one when WHMCS already has what you need, or when there is nothing to provision.
- A module already exists. WHMCS ships server modules for common control panels. Check those, and ask the vendor of the remote system, before paying for custom work. Pterodactyl, for example, publishes a free one, which my guide on connecting Pterodactyl to WHMCS sets up step by step.
- Staff deliver the product by hand. The product documentation describes an Auto Release module that simulates activation and sends a welcome email.
- You only need a side effect. Adding a DNS record or a chat message after provisioning is a hook on
AfterModuleCreate, not a module.
A custom module also has limits you should know before you start. It can do only what the remote system's API allows. A failed automated action waits in the Module Queue until a person retries it. And a success from _CreateAccount marks the service Active and sends the welcome email, so a module that reports success too early tells customers about accounts that do not exist yet.
What else do people ask about WHMCS provisioning modules?
Does WHMCS retry a failed module action?
No. The Module Queue documentation says it plainly: "WHMCS will not automatically retry a failed action. You must click Retry to attempt the failed action again." Plan for that. A module that fails on a temporary network error leaves a service waiting until someone opens Utilities > Module Queue, so retry inside your own API client where a retry is safe.
What must a module function return?
The word success, and nothing else, when the action worked. Anything else counts as a failure, and the documentation asks for "a user understandable error message, as it will display to staff users". Admins get a confirmation or the error, by email when the action came from automation. Errors from _ChangePassword reach the client too, so keep credentials and raw API replies out of them.
Can a provisioning module bill by usage?
Yes, through two different functions. _UsageUpdate runs a daily import of disk and bandwidth from the server, which feeds the usage figures in the client and admin areas and any disk or bandwidth overage billing enabled on the product. _MetricProvider collects named metric stats instead, and WHMCS can turn that stored usage into invoice line items when it is enabled in the Invoicing section of General Settings.
What happens the moment _CreateAccount returns success?
The service status changes to Active and the Product Welcome Email goes out. Nothing checks it afterwards. So return success only once the remote system has confirmed the account exists and the credentials in that email will work. A module that reports success while the panel is still building the account sends the customer a login that fails.
If you want one built, scope and limits are on my WHMCS custom module development page and my post on module cost explains what drives the hours, with ongoing updates covered by WHMCS support and maintenance; the scoping call of about 30 minutes is free and a written quote follows within one business day.
Last reviewed 30 October 2026. Disclosure: I am a freelance WHMCS developer and I sell custom provisioning modules at $55-$65 per hour or a fixed quote, so I am not neutral. I am CTO at ElySpace, a freelance developer for Marmaids LLC, and founder of WHMCSPilot and MagizAI.