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

  1. Copy the module folder to your WHMCS installation:
    cp -r whmcs/modules/servers/demo /path/to/whmcs/modules/servers/
    
  2. Set proper permissions:
    chmod 644 /path/to/whmcs/modules/servers/demo/demo.php
    
  3. Configure the server in WHMCS Admin → Setup → Servers

  4. Create a product using your module

  5. 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!