The WHMCS API is the back door that turns WHMCS from a billing app into a platform. Anything you can do in the admin UI, you can do via the API. Automate signups, sync billing data, build custom client portals, integrate with external CRMs, write management scripts — once you understand the API, your WHMCS install is no longer a closed box.
I use the API every week. Here's the practical guide — not a reference dump, but the patterns that actually work in production.
WHMCS has two APIs — pick the right one
This trips up almost everyone the first week.
- Internal API — call directly from PHP code running inside WHMCS (hooks, modules, custom pages). Function:
localAPI('CommandName', $params). No network round-trip, no authentication. - External / Admin API — call from outside WHMCS over HTTPS. Same commands, but authenticated. URL:
https://yourwhmcs.com/includes/api.php.
Both expose the same ~200 commands. Use internal when you're writing code that ships with WHMCS; use external for anything sitting on a different server, in a different language, or behind a different domain.
Authentication — do it once, do it right
WHMCS gives you two ways to authenticate the external API:
- API Credentials (Identifier + Secret) — modern method. Each API user is a separate admin with a dedicated identifier/secret pair and granular role permissions.
- Username + Password — legacy method. Tied to an admin login. The password is sent on every request. Don't use this.
Create API credentials at Setup → Staff Management → API Credentials → Generate New API Credential. Pick a role that's restricted to the commands you'll actually call — never give an API key full admin rights. I keep separate credentials for different purposes (analytics, signup automation, support tooling) so I can revoke one without breaking the others.
Your first API call — the no-framework version
From any server that can reach your WHMCS install:
<?php
$ch = curl_init('https://yourwhmcs.com/includes/api.php');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'identifier' => getenv('WHMCS_API_IDENTIFIER'),
'secret' => getenv('WHMCS_API_SECRET'),
'action' => 'GetClientsDetails',
'clientid' => 42,
'responsetype' => 'json',
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($response['result'] === 'success') {
echo "Client: {$response['firstname']} {$response['lastname']}\n";
} else {
echo "Error: {$response['message']}\n";
}
That's the entire shape. Every API call follows the same pattern:
- POST to
/includes/api.php. - Include
identifier+secret+action. - Add command-specific parameters.
- Set
responsetype=json(default is the worst format, XML wrapped in PHP-serialised arrays). - Check
$response['result']for'success'or'error'.
The 15 commands I actually use
WHMCS exposes ~200 commands but I use maybe 15 regularly:
| Command | Use case |
|---|---|
AddClient | Create a client account from a signup form on another site. |
GetClientsDetails | Look up a client by ID or email. |
UpdateClient | Sync data from your CRM back to WHMCS. |
GetClients | List clients with filtering (use sparingly — paginate). |
AddOrder | Place an order programmatically. |
GetOrders | List orders by status, date, client. |
AcceptOrder | Mark a pending order as accepted, triggering provisioning. |
GetInvoices | Pull invoices for reporting / accounting sync. |
AddInvoicePayment | Record a payment received outside WHMCS. |
SendEmail | Trigger any WHMCS email template programmatically. |
GetClientsProducts | Pull a client's active services. |
ModuleCreate / Suspend / Unsuspend / Terminate | Run lifecycle actions on a service from outside. |
OpenTicket / AddTicketReply / GetTickets | Build custom helpdesk integrations. |
GetTransactions | Pull payment transactions for accounting. |
UpdateInvoice | Apply credit, mark paid, change due date. |
Full reference: developers.whmcs.com/api/api-index/.
A real example — accept signups from a marketing site
Scenario I get asked to build all the time: marketing site on Webflow / Astro / Next.js has a signup form, but the actual customer record needs to live in WHMCS so billing can take over from there.
Backend handler (PHP, Node, anything that can POST):
function createWhmcsClient(array $data): array
{
$payload = [
'identifier' => getenv('WHMCS_API_IDENTIFIER'),
'secret' => getenv('WHMCS_API_SECRET'),
'action' => 'AddClient',
'responsetype' => 'json',
'firstname' => $data['first_name'],
'lastname' => $data['last_name'],
'email' => $data['email'],
'address1' => $data['address'] ?? 'Not provided',
'city' => $data['city'] ?? 'Not provided',
'state' => $data['state'] ?? 'Not provided',
'postcode' => $data['postcode'] ?? '00000',
'country' => $data['country'] ?? 'US',
'phonenumber'=> $data['phone'] ?? '',
'password2' => bin2hex(random_bytes(12)), // generate random
'skipvalidation' => false,
'noemail' => false, // WHMCS sends the welcome + password-reset email
];
$ch = curl_init('https://yourwhmcs.com/includes/api.php');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$raw = curl_exec($ch);
curl_close($ch);
return json_decode($raw, true);
}
Notice password2 — you set a temporary random password and ask WHMCS to email a password-reset link by setting noemail=false. Customer never sees the temporary password; they set their own via the email.
Rate limits and pagination — the production realities
WHMCS doesn't ship rate limits by default, but your server's PHP-FPM and database will. Two realities to plan for:
- Don't loop API calls in tight code. If you need to pull 500 invoices, use the list commands (
GetInvoices) with pagination (limitstart,limitnum), not 500 individualGetInvoicecalls. - Spread bulk work over time. A nightly sync that hits the API 10 times is fine. A real-time sync that hits the API on every Webflow page view will crater your WHMCS server.
Pagination shape:
$page = 0;
$pageSize = 100;
while (true) {
$response = callApi('GetInvoices', [
'limitstart' => $page * $pageSize,
'limitnum' => $pageSize,
'status' => 'Unpaid',
]);
if ($response['result'] !== 'success' || empty($response['invoices']['invoice'])) {
break;
}
foreach ($response['invoices']['invoice'] as $invoice) {
processInvoice($invoice);
}
$page++;
if (count($response['invoices']['invoice']) < $pageSize) break;
}
Error handling — what failure looks like
WHMCS returns errors as JSON with result: error and a message field. Two error categories:
- API errors — bad credentials, unknown command, missing required field. Stable, machine-readable.
- Business errors — "Client not found", "Email already exists", "Product not available". Still returned as
errorbut mean something different.
Wrap calls in a thin client that surfaces both clearly:
class WhmcsApiException extends \Exception {}
function callApi(string $action, array $params = []): array
{
$payload = array_merge($params, [
'identifier' => getenv('WHMCS_API_IDENTIFIER'),
'secret' => getenv('WHMCS_API_SECRET'),
'action' => $action,
'responsetype' => 'json',
]);
$ch = curl_init('https://yourwhmcs.com/includes/api.php');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$raw = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if ($raw === false) throw new WhmcsApiException("Network error: $err");
$response = json_decode($raw, true);
if (!$response) throw new WhmcsApiException("Non-JSON response: $raw");
if (($response['result'] ?? '') !== 'success') {
throw new WhmcsApiException("API error ({$action}): " . ($response['message'] ?? 'unknown'));
}
return $response;
}
Internal API — when you're inside WHMCS
From a hook, addon, or any PHP file that's been bootstrapped by WHMCS, use localAPI:
$response = localAPI('GetClientsDetails', ['clientid' => 42]);
if ($response['result'] === 'success') {
// ...
}
No identifier/secret needed (you're already inside trusted code). Same commands, same response format. I prefer localAPI over direct DB queries in hooks because it goes through the same validation WHMCS uses everywhere else — less chance of corrupting data.
How to verify the API is working
- The simplest healthcheck: call
WhmcsDetails. Returns the WHMCS version + license info. If this works, your credentials are good and the API endpoint is reachable. - Utilities → Logs → API Log — shows every external API call (request, response, source IP). Filter by date to debug specific issues.
- Test from your actual deployment environment (not your laptop). Firewall rules, DNS, and TLS can all differ between dev and production.
Common pitfalls
"All my calls return The request was made from an unauthorized IP address." WHMCS API credentials can be IP-restricted. Either add your caller's IP to the allowlist, or remove the restriction if you've already authenticated some other way.
"responsetype isn't set, response looks weird." Default response format is XML-ish nested arrays. Always set responsetype=json.
"AddOrder fails with cryptic error." The order command has 20+ valid combinations of pid/configoptions/customfields/promocode. Read the AddOrder docs twice. Start with a minimal call and add fields one at a time.
"My API caller's IP changes (Docker, dynamic infrastructure)." Don't IP-restrict. Restrict by role permissions instead and rotate secrets regularly.
"Internal API and external API behave differently for the same command." A few commands (notably ticket operations) have slightly different return shapes. Test both paths if you're using both.
My take — when to use the API vs. direct DB queries
WHMCS exposes its database. You could just query tblclients directly from a sibling script. Don't.
- The API runs WHMCS's validation, triggers hooks, updates derived tables. Direct DB writes skip all of that, then you've corrupted state subtly and you'll find out months later.
- The schema can change between WHMCS versions. The API is stable.
- Logging, audit trail, permission checks — you get all of these for free via the API.
The one exception: read-only reporting queries that only touch leaf tables (transactions, invoices, clients). For those, raw Capsule queries or even plain SQL are faster and don't add risk.
Going further
I build WHMCS API integrations end-to-end: custom signup flows, accounting sync, CRM integrations, partner portals. If your business needs WHMCS to talk to systems it wasn't designed for, tell me what you need and I'll send a quote in 24 hours.