Building WHMCS Provisioning Modules: A Complete Developer's Guide
Learn how to create professional WHMCS provisioning modules that automate service delivery for your hosting business.
Introduction
If you’re running a hosting business with WHMCS, you’ve likely encountered situations where you need to integrate third-party services or custom solutions. Whether it’s connecting to a cloud provider, security service, or any SaaS platform, WHMCS provisioning modules are the key to automating these integrations.
In this guide, I’ll walk you through building a complete WHMCS provisioning module from scratch. By the end, you’ll have a production-ready template that handles account creation, suspension, termination, and more.
📦 Get the complete source code: github.com/Stdubic/whmc-module
What is a WHMCS Provisioning Module?
A provisioning module (also called a “server module”) is a PHP file that tells WHMCS how to communicate with an external service. When a customer purchases a product, the module automatically:
- Creates their account on your platform
- Suspends access when payment is overdue
- Unsuspends when payment is received
- Terminates the account when cancelled
This automation eliminates manual work and provides instant service delivery.
Module Structure
A WHMCS module is surprisingly simple—it’s a single PHP file with specially named functions. Here’s the basic structure:
whmcs/modules/servers/yourmodule/
└── yourmodule.php
The filename must match the folder name, and all functions must be prefixed with the module name.
Required Functions
| Function | Purpose |
|---|---|
yourmodule_MetaData() |
Module information |
yourmodule_ConfigOptions() |
Product configuration fields |
yourmodule_CreateAccount() |
Provision new service |
yourmodule_SuspendAccount() |
Suspend service |
yourmodule_UnsuspendAccount() |
Reactivate service |
yourmodule_TerminateAccount() |
Delete service |
Step 1: Module Metadata
Every module starts with metadata that tells WHMCS about your module:
<?php
if (!defined('WHMCS')) {
die('This file cannot be accessed directly');
}
function demo_MetaData(): array
{
return [
'DisplayName' => 'Demo Module',
'APIVersion' => '1.1',
'RequiresServer' => true,
];
}
The security check at the top prevents direct access to the file—always include this!
Step 2: Configuration Options
Configuration options appear when creating products in WHMCS. These let administrators customize how the module behaves for different products:
function demo_ConfigOptions(): array
{
return [
'plan_type' => [
'FriendlyName' => 'Plan Type',
'Type' => 'dropdown',
'Options' => [
'basic' => 'Basic',
'standard' => 'Standard',
'premium' => 'Premium',
],
'Default' => 'standard',
],
'feature_enabled' => [
'FriendlyName' => 'Enable Extra Feature',
'Type' => 'yesno',
'Description' => 'Enable additional features',
],
'max_items' => [
'FriendlyName' => 'Max Items',
'Type' => 'text',
'Size' => 10,
'Default' => '10',
],
];
}
These values are accessible in your functions as $params['configoption1'], $params['configoption2'], etc.
Step 3: The CreateAccount Function
This is where the magic happens. When an order is accepted, WHMCS calls this function to provision the service:
function demo_CreateAccount(array $params): string
{
try {
// Extract parameters
$serviceId = $params['serviceid'];
$domain = $params['domain'];
$email = $params['clientsdetails']['email'];
$planType = $params['configoption1'];
// Initialize your API client
$api = new DemoApiHelper($params);
// Create the account
$result = $api->createAccount([
'email' => $email,
'domain' => $domain,
'plan' => $planType,
]);
// Check for errors
if (isset($result['error'])) {
return 'Error: ' . $result['error'];
}
// Store the account ID for later use
demo_StoreCredentials($serviceId, [
'account_id' => $result['account_id'],
'status' => 'active',
]);
return 'success';
} catch (Exception $e) {
return 'Error: ' . $e->getMessage();
}
}
Important: Return 'success' (lowercase string) on success, or an error message on failure.
Step 4: Suspend and Unsuspend
These functions handle billing-related status changes:
function demo_SuspendAccount(array $params): string
{
try {
$credentials = demo_GetCredentials($params['serviceid']);
if (empty($credentials['account_id'])) {
return 'success'; // Nothing to suspend
}
$api = new DemoApiHelper($params);
$result = $api->suspendAccount($credentials['account_id']);
if (isset($result['error'])) {
return 'Error: ' . $result['error'];
}
return 'success';
} catch (Exception $e) {
return 'Error: ' . $e->getMessage();
}
}
function demo_UnsuspendAccount(array $params): string
{
try {
$credentials = demo_GetCredentials($params['serviceid']);
$api = new DemoApiHelper($params);
$result = $api->unsuspendAccount($credentials['account_id']);
if (isset($result['error'])) {
return 'Error: ' . $result['error'];
}
return 'success';
} catch (Exception $e) {
return 'Error: ' . $e->getMessage();
}
}
Step 5: Terminate Account
Called when a service is permanently cancelled:
function demo_TerminateAccount(array $params): string
{
try {
$credentials = demo_GetCredentials($params['serviceid']);
if (empty($credentials['account_id'])) {
return 'success'; // Already terminated
}
$api = new DemoApiHelper($params);
$result = $api->deleteAccount($credentials['account_id']);
if (isset($result['error'])) {
return 'Error: ' . $result['error'];
}
// Clear stored credentials
demo_ClearCredentials($params['serviceid']);
return 'success';
} catch (Exception $e) {
return 'Error: ' . $e->getMessage();
}
}
Step 6: Building an API Helper Class
Centralizing your API logic in a helper class keeps your code clean and maintainable:
class DemoApiHelper
{
private $apiEndpoint;
private $apiKey;
private $timeout = 30;
public function __construct(array $params)
{
$this->apiEndpoint = rtrim($params['serverhostname'], '/');
$this->apiKey = $params['serverpassword'];
if (empty($this->apiKey)) {
throw new Exception('API key is not configured');
}
}
public function createAccount(array $data): array
{
return $this->makeRequest('POST', '/accounts', $data);
}
public function suspendAccount(string $accountId): array
{
return $this->makeRequest('POST', "/accounts/{$accountId}/suspend");
}
private function makeRequest(string $method, string $endpoint, array $data = []): array
{
$url = $this->apiEndpoint . $endpoint;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $this->apiKey,
'Content-Type: application/json',
],
]);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$decoded = json_decode($response, true);
if ($httpCode >= 400) {
return ['error' => $decoded['message'] ?? 'Request failed'];
}
return $decoded ?? ['success' => true];
}
}
Step 7: Storing Data in Custom Fields
WHMCS custom fields are perfect for storing external account IDs and credentials:
function demo_StoreCredentials(int $serviceId, array $credentials): void
{
foreach ($credentials as $fieldName => $value) {
demo_UpdateCustomField($serviceId, $fieldName, $value);
}
}
function demo_UpdateCustomField(int $serviceId, string $fieldName, $value): void
{
$productId = Capsule::table('tblhosting')
->where('id', $serviceId)
->value('packageid');
// Find or create the custom field
$field = Capsule::table('tblcustomfields')
->where('fieldname', $fieldName)
->where('relid', $productId)
->where('type', 'product')
->first();
if (!$field) {
$fieldId = Capsule::table('tblcustomfields')->insertGetId([
'type' => 'product',
'relid' => $productId,
'fieldname' => $fieldName,
'fieldtype' => 'text',
'adminonly' => 'on',
]);
} else {
$fieldId = $field->id;
}
// Update or insert the value
Capsule::table('tblcustomfieldsvalues')->updateOrInsert(
['fieldid' => $fieldId, 'relid' => $serviceId],
['value' => $value]
);
}
Step 8: Adding Admin Buttons
Custom buttons let admins perform actions directly from the service page:
function demo_AdminCustomButtonArray(): array
{
return [
'View Details' => 'viewDetails',
'Resync Account' => 'resyncAccount',
];
}
function demo_viewDetails(array $params): string
{
$credentials = demo_GetCredentials($params['serviceid']);
// Redirect to external dashboard
header('Location: https://dashboard.example.com/' . $credentials['account_id']);
exit;
}
function demo_resyncAccount(array $params): string
{
// Fetch latest data from API and update local records
$api = new DemoApiHelper($params);
$result = $api->getAccountDetails($credentials['account_id']);
// Update local data...
return 'success';
}
Setting Up Local Development
Testing WHMCS modules requires a running WHMCS installation. Our template includes a Docker setup for easy local development:
# Clone the repository
git clone https://github.com/Stdubic/whmc-module.git
cd whmc-module
# Start the environment
./scripts/setup-local.sh
docker compose up -d
# Access WHMCS
open http://localhost:8088/admin/
The module files are mounted into the container, so changes take effect immediately—no restart needed!
Local Development URLs
| Service | URL |
|---|---|
| WHMCS Admin | http://localhost:8088/admin/ |
| WHMCS Client | http://localhost:8088/ |
| phpMyAdmin | http://localhost:8081/ |
| MailHog | http://localhost:8025/ |
Best Practices
1. Always Log API Calls
Use WHMCS’s built-in logging for debugging:
logModuleCall(
'demo',
'CreateAccount',
json_encode($params),
json_encode($response)
);
2. Handle Errors Gracefully
Never let exceptions crash silently:
try {
// Your code
} catch (Exception $e) {
logModuleCall('demo', 'Error', '', $e->getMessage());
return 'Error: ' . $e->getMessage();
}
3. Validate Before Acting
Check prerequisites before making API calls:
if (empty($params['domain'])) {
return 'Error: Domain is required';
}
4. Use Hooks for Extended Functionality
Hooks let you react to WHMCS events without modifying core files:
add_hook('AfterModuleCreate', 1, function ($vars) {
if ($vars['params']['server']['type'] !== 'demo') {
return;
}
logActivity('Demo: Service provisioned');
});
Deploying to Production
- Copy the module folder to your WHMCS installation:
cp -r whmcs/modules/servers/demo /path/to/whmcs/modules/servers/ - Set proper permissions:
chmod 644 /path/to/whmcs/modules/servers/demo/demo.php -
Configure the server in WHMCS Admin → Setup → Servers
-
Create a product using your module
- Test with a small order before going live
Conclusion
Building WHMCS provisioning modules opens up endless possibilities for automating your hosting business. With the template provided, you have a solid foundation that follows best practices and handles all the essential operations.
Ready to build your own module?
Clone the complete template from GitHub and start customizing:
🔗 github.com/Stdubic/whmc-module
The repository includes:
- ✅ Complete module template with all functions
- ✅ API helper class ready for customization
- ✅ Docker development environment
- ✅ Hook examples
- ✅ Comprehensive documentation
Resources
Have questions or need help with your WHMCS integration? Feel free to reach out or open an issue on GitHub!