On this page
WHMCS + Proxmox Integration: Automating VM Provisioning, Billing, and Management – A Complete Guide
Running a VPS hosting business means juggling provisioning, billing, and support. Doing it manually eats time and invites errors. By integrating WHMCS (the industry‑standard billing and automation platform) with Proxmox VE (a powerful open‑source virtualization platform), you can automate the entire lifecycle of a virtual machine—from order to termination—while keeping invoices in sync.
This guide walks you through a step‑by‑step setup of a WHMCS module for Proxmox, covering prerequisites, API configuration, module creation, product mapping, testing, and ongoing maintenance. Real code examples are included so you can copy‑paste and adapt them to your environment.
Prerequisites
Before you start, make sure you have:
| Item | Minimum version | Notes |
|---|---|---|
| WHMCS | 8.0+ | Latest stable release recommended |
| Proxmox VE | 7.0+ | Ensure the pve-api service is running |
| PHP | 7.4+ | WHMCS requires PHP 7.4 or newer |
| cURL extension | enabled | Used for API calls |
| A domain or subdomain pointing to your WHMCS install (optional but recommended for callbacks) | ||
| SSH root access to the Proxmox host (for initial API token creation) |
You should also have a basic understanding of WHMCS products, services, and hooks, as well as comfort editing PHP files.
1. Enable the Proxmox API
Proxmox provides a RESTful API that WHMCS will call to create, suspend, unsuspend, and terminate VMs.
1.1 Create an API Token
SSH into your Proxmox host and run:
# Replace 'whmcs' with any identifier you like
pveum token add whmcs whmcs-token --privilege=Administrator
You’ll receive output similar to:
token: whmcs!whmcs-token
value: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
Store the token ID (whmcs!whmcs-token) and the secret (a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6) securely; you’ll need them in WHMCS.
1.2 Verify API Access
Test the token with curl:
curl -k -H "Authorization: PVEAPIToken=whmcs!whmcs-token=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" \
https://YOUR_PROXMOX_HOST:8006/api2/json/nodes
If you see a JSON list of nodes, the token works.
2. Set Up WHMCS for Custom Modules
WHMCS looks for custom modules in /modules/servers/. We’ll create a new server module called proxmox.
2.1 Create the Module Directory
mkdir -p /path/to/whmcs/modules/servers/proxmox
2.2 Create the Main Module File
Create /path/to/whmcs/modules/servers/proxmox/proxmox.php with the following boilerplate:
<?php
/**
* WHMCS Proxmox Server Module
*
* @package WHMCS
* @subpackage Module/Server/Proxmox
*/
use WHMCS\Module\Server\Proxmox\ProxmoxAPI;
if (!defined("WHMCS")) {
die("This file cannot be accessed directly");
}
/**
* Module Configuration Array
*
* @return array
*/
function proxmox_config()
{
return [
'FriendlyName' => ['Type' => 'System', 'Value' => 'Proxmox VE'],
'api_host' => [
'FriendlyName' => 'Proxmox Host',
'Type' => 'text',
'Size' => '25',
'Description' => 'Hostname or IP of your Proxmox VE server (e.g., proxmox.example.com)',
],
'api_port' => [
'FriendlyName' => 'API Port',
'Type' => 'text',
'Size' => '5',
'Default' => '8006',
'Description' => 'Port on which the Proxmox API listens (default 8006)',
],
'api_token_id' => [
'FriendlyName' => 'API Token ID',
'Type' => 'text',
'Size' => '30',
'Description' => 'Token ID in the format user!token-name',
],
'api_token_secret' => [
'FriendlyName' => 'API Token Secret',
'Type' => 'text',
'Size' => '50',
'Description' => 'Secret value generated when creating the token',
],
'node' => [
'FriendlyName' => 'Default Node',
'Type' => 'text',
'Size' => '20',
'Description' => 'Proxmox node where VMs will be created (leave blank to let WHMCS choose)',
],
];
}
/**
* Module Activation Hook
*
* @param array $params
*/
function proxmox_activate($params)
{
// No special activation needed
}
/**
* Module Deactivation Hook
*
* @param array $params
*/
function proxmox_deactivate($params)
{
// No special deactivation needed
}
/**
* Create a VM
*
* @param array $params
* @return string
*/
function proxmox_create_account($params)
{
$serverParams = $params['server'];
$api = new ProxmoxAPI(
$serverParams['api_host'],
$serverParams['api_port'],
$serverParams['api_token_id'],
$serverParams['api_token_secret']
);
// Extract product configurable options
$vmid = $params['configoption1']; // Assume configoption1 holds desired VMID (or 0 for auto)
$hostname = $params['domain']; // WHMCS passes the domain as hostname
$cores = $params['configoption2']; // Number of CPU cores
$ram = $params['configoption3'] * 1024; // WHMCS stores RAM in MB, convert to MB for API
$disk = $params['configoption4'] * 1024; // Disk in GB -> MB
$template = $params['configoption5']; // Template ID (e.g., 'local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.gz')
// If VMID not set, let Proxmox assign next available
if (empty($vmid) || $vmid == 0) {
$vmid = $api->getNextVmid();
}
// Create the VM (using QEMU for simplicity)
$api->createVm(
$vmid,
$hostname,
$cores,
$ram,
$disk,
$template,
$serverParams['node'] ?? null
);
// Return success
return "success";
}
/**
* Suspend a VM
*
* @param array $params
* @return string
*/
function proxmox_suspend_account($params)
{
$serverParams = $params['server'];
$api = new ProxmoxAPI(
$serverParams['api_host'],
$serverParams['api_port'],
$serverParams['api_token_id'],
$serverParams['api_token_secret']
);
$vmid = $params['serviceid']; // WHMCS passes the service ID as VMID
$api->setVmStatus($vmid, 'stop');
return "success";
}
/**
* Unsuspend a VM
*
* @param array $params
* @return string
*/
function proxmox_unsuspend_account($params)
{
$serverParams = $params['server'];
$api = new ProxmoxAPI(
$serverParams['api_host'],
$serverParams['api_port'],
$serverParams['api_token_id'],
$serverParams['api_token_secret']
);
$vmid = $params['serviceid'];
$api->setVmStatus($vmid, 'start');
return "success";
}
/**
* Terminate a VM
*
* @param array $params
* @return string
*/
function proxmox_terminate_account($params)
{
$serverParams = $params['server'];
$api = new ProxmoxAPI(
$serverParams['api_host'],
$serverParams['api_port'],
$serverParams['api_token_id'],
$serverParams['api_token_secret']
);
$vmid = $params['serviceid'];
$api->deleteVm($vmid);
return "success";
}
/**
* Change Package (upgrade/downgrade)
*
* @param array $params
* @return string
*/
function proxmox_change_package($params)
{
// For simplicity, we treat change as a suspend + create with new specs
// In a production module you would resize the existing VM.
proxmox_terminate_account($params);
return proxmox_create_account($params);
}
/**
* Admin Dashboard Output (optional)
*
* @param array $params
* @return string
*/
function proxmox_admin_panel($params)
{
return '<div class="panel panel-default"><div class="panel-heading">Proxmox VE Server</div><div class="panel-body">Module is active.</div></div>';
}
2.3 Helper Class – ProxmoxAPI
Create a separate file /path/to/whmcs/modules/servers/proxmox/proxmoxapi.php to encapsulate API calls:
<?php
if (!defined("WHMCS")) {
die("This file cannot be accessed directly");
}
class ProxmoxAPI
{
protected $host;
protected $port;
protected $tokenId;
protected $tokenSecret;
protected $baseUrl;
public function __construct($host, $port = 8006, $tokenId, $tokenSecret)
{
$this->host = rtrim($host, '/');
$this->port = $port;
$this->tokenId = $tokenId;
$this->tokenSecret = $tokenSecret;
$this->baseUrl = sprintf("https://%s:%d/api2/json", $this->host, $this->port);
}
/**
* Make a GET request
*/
private function get($endpoint)
{
$url = $this->baseUrl . $endpoint;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // For self‑signed certs; adjust in production
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: PVEAPIToken={$this->tokenId}={$this->tokenSecret}",
"Content-Type: application/json"
]);
$result = curl_exec($ch);
curl_close($ch);
return json_decode($result, true);
}
/**
* Make a POST request
*/
private function post($endpoint, $data = [])
{
$url = $this->baseUrl . $endpoint;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: PVEAPIToken={$this->tokenId}={$this->tokenSecret}",
"Content-Type: application/json"
]);
$result = curl_exec($ch);
curl_close($ch);
return json_decode($result, true);
}
/**
* Get next available VMID
*/
public function getNextVmid()
{
$res = $this->get('/cluster/nextid');
return $res['data'] ?? null;
}
/**
* Create a VM (QEMU)
*/
public function createVm($vmid, $hostname, $cores, $ramMb, $diskMb, $template, $node = null)
{
$node = $node ?? $this->getFirstNode();
$payload = [
'vmid' => $vmid,
'name' => $hostname,
'cores' => $cores,
'memory' => $ramMb,
'net0' => 'virtio,bridge=vmbr0',
'scsi0' => sprintf("%s:%d", $template, $diskMb),
'ostype' => 'l26',
'boot' => 'order=scsi0;net0',
];
$this->post("/nodes/{$node}/qemu", $payload);
}
/**
* Set VM status (start/stop/shutdown)
*/
public function setVmStatus($vmid, $action)
{
$node = $this->getNodeForVm($vmid);
$this->post("/nodes/{$node}/qemu/{$vmid}/status/{$action}", []);
}
/**
* Delete a VM
*/
public function deleteVm($vmid)
{
$node = $this->getNodeForVm($vmid);
$this->post("/nodes/{$node}/qemu/{$vmid}", ['delete' => 1]);
}
/**
* Retrieve the first node (used when none is specified)
*/
private function getFirstNode()
{
$res = $this->get('/nodes');
if (!empty($res['data'])) {
return $res['data'][0]['node'];
}
throw new Exception('No nodes found in Proxmox cluster');
}
/**
* Find which node a VM resides on
*/
private function getNodeForVm($vmid)
{
$res = $this->get('/cluster/resources?type=vm');
foreach ($res['data'] as $vm) {
if ((int)$vm['vmid'] === (int)$vmid) {
return $vm['node'];
}
}
throw new Exception("VMID {$vmid} not found on any node");
}
}
Security Note: The example disables SSL verification (
CURLOPT_SSL_VERIFYPEER => false) for simplicity. In production, install a valid certificate on your Proxmox host and set this option totrue, or add the CA certificate to cURL’s trust store.
3. Register the Server in WHMCS
- Log into your WHMCS admin area.
- Navigate to Setup → Products/Services → Servers.
- Click Add New Server.
- Fill in:
- Hostname: Your Proxmox host (e.g.,
proxmox.example.com) - Port:
8006 - Type: Select Proxmox VE (the friendly name we defined).
- Username: Your API Token ID (e.g.,
root!pam@whmcs-token– note WHMCS expects the token ID exactly as created). - Password: Your API Token Secret.
- Hostname: Your Proxmox host (e.g.,
- Save. WHMCS will test the connection; you should see a “Connection Successful” message.
4. Create a Product/Service
4.1 Define Configurable Options
Go to Setup → Products/Services → Configurable Options and create options that map to VM resources:
| Option Name | Type | Description |
|---|---|---|
| CPU Cores | Dropdown | 1, 2, 4, 8 |
| RAM (MB) | Dropdown | 512, 1024, 2048, 4096 |
| Disk (GB) | Dropdown | 10, 20, 40, 80 |
| OS Template | Dropdown | List of templates available on your Proxmox storage (e.g., local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.gz) |
| Desired VMID (optional) | Text | Leave blank for auto‑assign |
4.2 Create the Product
- Setup → Products/Services → Products/Services → Create a New Product.
- Product Type: Hosting Account.
- Name: e.g., “Proxmox VPS – Basic”.
- Module Settings: Choose the server you just added (Proxmox VE).
- Assign Configurable Options: Add the options you created.
- Pricing: Set monthly/annual rates as desired.
- Save.
5. Automation: Cron and Hooks
WHMCS relies on its cron to trigger provisioning, suspension, and termination based on order status and service dates.
5.1 Ensure Cron Is Running
Add the following to your server’s crontab (crontab -e):
*/5 * * * * php /path/to/whmcs/admin/cron.php
This runs every five minutes, checking for pending orders, suspensions, etc.
5.2 Optional: Provisioning Hook for Immediate Creation
If you want VMs to be created immediately upon payment (instead of waiting for the next cron), create a hook file /path/to/whmcs/includes/hooks/provisionvm.php:
<?php
if (!defined("WHMCS")) {
die("This file cannot be accessed directly");
}
add_hook('AfterPaymentConfirmation', 1, function($vars) {
$invoiceId = $vars['invoiceid'];
// Load invoice details to see if it's a hosting product
$invoice = Capsule::table('tblinvoices')
->join('tblinvoiceitems', 'tblinvoiceitems.invoiceid', '=', 'tblinvoices.id')
->where('tblinvoices.id', $invoiceId)
->first();
if ($invoice && $invoice->type == 'Hosting Account') {
$serviceId = $invoice->serviceid;
// Trigger provisioning via WHMCS API
$result = localAPI('AcceptOrder', ['serviceid' => $serviceId]);
// Optionally log
if ($result['result'] == 'success') {
logActivity("Proxmox VM provisioned immediately after payment for service ID {$serviceId}");
}
}
});
6. Testing the Flow
- Place a Test Order (via WHMCS client area or admin → Add Order).
- Complete payment (use a test gateway or mark as paid manually).
- Watch the WHMCS admin log under Utilities → Logs → Module Log for entries like:
proxmox_create_account: successProxmox API call to /nodes/pve1/qemu returned ...
- In Proxmox UI, verify a new VM appears with the correct hostname, CPU, RAM, and disk.
- Test suspension: edit the service status to Suspended → check that the VM stops.
- Test unsuspension → VM starts.
- Test termination → VM disappears from Proxmox.
If any step fails, check:
- WHMCS Module Log.
- Proxmox
/var/log/pveproxy/access.logand/var/log/pvedaemon/[plugin].log. - Ensure the API token has
Administratorprivileges or at leastVM.AuditandVM.Allocateon the target node.
7. Comparison: WHMCS + Proxmox vs Alternatives
| Feature | WHMCS + Proxmox | cPanel/WHM + WHMCS | SolusVM | Virtualizor |
|---|---|---|---|---|
| Licensing Cost | Free (Proxmox) + WHMCS license | cPanel license + WHMCS | Paid per node | Paid per node |
| Hypervisor | KVM/LXC (open) | Primarily KVM via Virtuozzo/OpenVZ (cPanel) | KVM/LXC | KVM/LXC |
| API Maturity | Stable REST API, well documented | WHM API (stable) | API available but less extensive | API available |
| Billing Automation | Full WHMCS integration | WHMCS works but limited to cPanel features | Requires third‑party billing bridge | Requires third‑party billing bridge |
| Scalability | Cluster‑ready, HA possible | Scaling requires additional licenses | Good for VPS only | Good for VPS only |
| Learning Curve | Moderate (need to understand Proxmox) | Low if already using cPanel | Low‑moderate | Low‑moderate |
| Community Support | Strong open‑source community | Large commercial community | Niche | Niche |
For a pure VPS/reseller business focused on cost‑effectiveness and flexibility, WHMCS + Proxmox offers the best balance of zero hypervisor licensing and powerful automation.
8. Ongoing Maintenance & Best Practices
| Task | Frequency | Details |
|---|---|---|
| Update Proxmox | Monthly | Follow the Proxmox update schedule; test updates on a staging node first. |
| WHMCS Updates | As released | Always backup before upgrading; test module compatibility. |
| API Token Rotation | Every 90 days | Generate a new token, update WHMCS server config, delete old token. |
| Monitor VM Health | Ongoing | Use Proxmox’s built‑in monitoring or external tools (e.g., Netdata) to alert on high CPU/RAM/disk usage. |
| Backup Strategy | Daily/Weekly | Configure Proxmox backup jobs to a remote storage (NFS, Ceph, or off‑site S3). |
| Security Hardening | Quarterly | Disable root SSH login, enable 2FA for PAM, keep firewall rules tight, and restrict API token IPs if possible. |
| Documentation | As needed | Keep an internal wiki of your product configurations, template IDs, and any custom hooks. |
9. Troubleshooting Common Issues
| Symptom | Likely Cause | Fix |
|---|---|---|
| Module returns “Invalid credentials” | Token ID/secret mismatch or missing ! separator |
Verify token format: username!token-name. Ensure no extra spaces. |
| VM creation fails with “storage not found” | Template path incorrect | List storage with pvesm list and adjust the template configurable option. |
| Cron does not trigger provisioning | Cron not running or cron.php permissions wrong |
Check /var/log/syslog for cron entries; ensure php CLI path is correct. |
| VM suspends but does not stop | Using shutdown instead of stop API call |
Ensure suspend function calls /status/stop. |
| After termination, VMID remains in WHMCS | Terminate hook not deleting associated service | Verify that the terminate function returns success and that WHMCS service status updates to “Cancelled”. |
| SSL handshake failure | Self‑signed certificate and verification enabled | Either add Proxmox CA to trusted certs or set CURLOPT_SSL_VERIFYPEER => false (not recommended for production). |
10. Conclusion
Integrating WHMCS with Proxmox VE transforms a manual VPS provisioning workflow into a seamless, automated operation. By following the steps above—setting up a secure API token, building a lightweight WHMCS server module, mapping configurable options, and leveraging WHMCS cron—you can offer your customers instant VM deployment, accurate billing, and reliable lifecycle management without the overhead of proprietary hypervisor licenses.
Whether you’re a startup launching your first VPS line or an established host looking to cut costs, this stack provides a solid, open‑source foundation that scales with your business.
For more tutorials on hosting automation, billing, and server management, keep an eye on mahbuburriad.com.
FAQ
Q1: Do I need a paid Proxmox license to use the API?
A: No. Proxmox VE is open source; its REST API is freely available in the community edition. Only the optional Proxmox Backup Server and certain enterprise repositories require a subscription.
Q2: Can I manage LXC containers instead of QEMU VMs with this module?
A: Absolutely. The helper class can be adjusted to call /nodes/{node}/lxc endpoints and use LXC‑specific settings (e.g., features: nesting=1). The provisioning function would change the payload accordingly.
Q3: How do I handle multiple Proxmox nodes in a cluster?
A: Leave the “Default Node” field blank in the WHMCS server configuration. The module’s getFirstNode() helper will pick the first node, but you can enhance it to select the node with the most free resources by querying /nodes/{node}/status.
Q4: Is it possible to resize an existing VM without deleting it?
A: Yes, but it requires additional API calls (stop, resize disk, start). For a production‑ready module you would implement a resize_account function that powers off the VM, updates the disk or memory settings via the API, then restarts it.
Q5: What about backups and snapshots—can WHMCS trigger those?
A: WHMCS does not have native actions for snapshots, but you can add custom admin actions or hooks that call the Proxmox snapshot API (/nodes/{node}/qemu/{vmid}/snapshot). This can be exposed as a configurable add‑on product.
Word count: ~1,730