Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Php Whmcs Api Laravel Package

darthsoup/php-whmcs-api

A lightweight PHP client for the WHMCS API, offering fluent methods to query clients, orders, invoices, tickets, and more. Simplifies authentication and API responses so you can integrate WHMCS billing and support data into PHP or Laravel apps with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is a thin, GitLabPHP/Client-inspired HTTP client for WHMCS. It collapses WHMCS's single POST /includes/api.php endpoint into resource objects (clients(), orders(), invoices(), etc.), handles the JSON envelope, and lets you inject any PSR-7-compatible HTTP client.

Minimum setup

  1. Install the package plus an HTTP client implementation if your project does not already have one:
composer require darthsoup/php-whmcs-api

# Only if you don't already have a PSR-7/PSR-18 or HTTPlug implementation:
composer require guzzlehttp/guzzle php-http/guzzle7-adapter

Check the package's composer.json to see whether it uses HTTPlug discovery or a PSR-18 client, and install the matching adapter.

  1. On the WHMCS side:
  • Create API credentials under Setup > Staff Management > API Credentials (WHMCS 7.2+). Older installations use an admin username/password.
  • Add the egress IPs of your application, cron jobs, queue workers, and containers to Setup > General Settings > Security > API IP Access Restriction.
  • Give the API admin a least-privilege role. API credentials inherit the admin's permissions, so a read-only role cannot create orders or apply payments.
  • Turn on API logging under Utilities > Logs > API Log while integrating.
  1. Create a client and make a read call:
use DarthSoup\WhmcsApi\Client;

$whmcs = new Client();
$whmcs->setUrl('https://billing.example.com/'); // WHMCS root, not /includes/api.php
$whmcs->setCredentials($apiIdentifier, $apiSecret);

$clients = $whmcs->clients()->all(['limitnum' => 25]);

The first useful end-to-end slice is: authenticate, fetch one client by ID, then fetch that client's orders/invoices. If you can do that, the rest of the resource API is mostly parameter mapping.

Where to look first

  • src/Client.php — URL/credential setters, HTTP client injection (createWithHttpClient or equivalent), and the magic __call/api() resource resolution.
  • src/Api/AbstractApi.php — how the action name, identifier/secret, responsetype=json, and parameters are assembled into the POST body.
  • src/Api/*.php — the supported resources and exact method names. WHMCS has no clean CRUD model, so expect a mix of GitLab-style methods (all(), show(), create()) and WHMCS action names (getClientsDetails, addOrder, addInvoicePayment).
  • Tests/fixtures — the fastest way to see the decoded response shape for each action.

If the exact setter or method name differs in your installed version, the files above are the source of truth; the integration pattern is the same.

Implementation Patterns

Bind the client as a singleton

Do not construct a client per call. In Laravel:

use DarthSoup\WhmcsApi\Client;

$this->app->singleton(Client::class, function () {
    $client = new Client();
    $client->setUrl(config('services.whmcs.url'));
    $client->setCredentials(
        config('services.whmcs.identifier'),
        config('services.whmcs.secret')
    );

    return $client;
});

Use one client per WHMCS installation. If you run multiple brands or regions, key clients by installation and resolve them from a small factory.

Wrap it in a repository/adapter

The package returns WHMCS-shaped arrays, not domain objects. Keep those arrays out of your application code:

final class WhmcsCustomerRepository
{
    public function __construct(private Client $whmcs) {}

    public function find(int $clientId): ?Customer
    {
        $response = $this->whmcs->clients()->show(['clientid' => $clientId]);

        if (($response['result'] ?? null) !== 'success') {
            return null;
        }

        return Customer::fromWhmcs($response);
    }
}

This also gives you one place to change when the package is upgraded or replaced.

Paginate explicitly

WHMCS list actions use limitstart and limitnum, and the default page size is often smaller than you expect:

$limit = 100;
$start = 0;
$all = [];

do {
    $page = $whmcs->clients()->all([
        'limitstart' => $start,
        'limitnum' => $limit,
    ]);

    $batch = $page['clients'] ?? [];
    $all = array_merge($all, $batch);
    $start += $limit;
} while (count($batch) === $limit);

Stop on a short page or when startnumber + numreturned >= totalresults, whichever the response exposes. Avoid huge limitnum values; WHMCS may time out generating large result sets.

Treat writes as jobs, not request-path calls

AddOrder, AddClient, AddInvoicePayment, OpenTicket, and AddTicketReply are mutating, non-idempotent actions. Put them behind queued jobs with:

  • An idempotency record in your own database keyed by your business operation.
  • Retries only for transport failures/5xx responses, never for a WHMCS result: error.
  • A manual reconciliation path for timeouts, because a timed-out AddOrder may still have created the order.

Use noinvoice / noemail parameters for staging tests so you do not email real customers or create real invoices.

Read/cache aggressively

Client details, product lists, and currency configuration change infrequently. Cache them for a few minutes to avoid making every page render depend on WHMCS latency. Invalidate on your own writes.

Use WHMCS hooks for events, not API polling

The WHMCS API is pull-only; there are no webhooks. For order/ticket/invoice events, add a WHMCS hook that calls your application's webhook endpoint, then use this client for lookups and follow-up actions. Polling GetTickets or GetOrders on a short interval is expensive and easy to get rate-limited by WHMCS/PHP execution limits.

Custom or missing actions

If an action is not wrapped, look at AbstractApi and either extend an existing API class or add a small custom class that calls the same generic request path. Keep custom code in your application rather than forking the package; the package is small enough that a subclass is usually sufficient.

Testing

Inject a mock HTTP client and return PSR-7 responses with JSON fixtures:

use Http\Mock\Client as MockClient;
use Nyholm\Psr7\Response;

$mock = new MockClient();
$mock->addResponse(new Response(200, [], json_encode([
    'result' => 'success',
    'totalresults' => 1,
    'clients' => [['id' => 1, 'firstname' => 'Ada']],
])));

$whmcs = Client::createWithHttpClient($mock);
$whmcs->setUrl('https://billing.test/');
$whmcs->setCredentials('id', 'secret');

If your installed version uses PSR-18 instead of HTTPlug, use Guzzle's MockHandler in the same way. Test your repository/adapter against realistic WHMCS payloads, including error envelopes and empty result sets.

Gotchas and Tips

  • WHMCS returns HTTP 200 for API errors. A failed call is still 200 OK with {"result":"error","message":"..."}. Always inspect result, and log message with the action and sanitized parameters. Do not rely on HTTP status codes or exceptions alone.

  • responsetype=json must be set. Without it, WHMCS returns XML and PSR-7 JSON parsing fails. The package should add it in AbstractApi; verify this first if you get XML parse errors.

  • Point the URL at the WHMCS root. Use https://billing.example.com/ or https://example.com/whmcs/, not https://.../includes/api.php. Watch trailing slashes; a double slash can produce confusing 404s on some setups.

  • API IP restriction is per source IP. The most common "works in Tinker, fails in the queue" bug is that the queue worker, cron host, or container has a different egress IP than your web server. Allowlist all of them.

  • Credentials live in the POST body. Use TLS verification, never disable it to work around a self-signed certificate, and redact identifier/secret from request logs and exception reports.

  • Permissions are inherited from the admin role. A read-only API role can fetch clients but will fail AddOrder or AddInvoicePayment. Create separate credentials per integration (billing sync, support tooling, provisioning) so you can rotate and scope them independently.

  • Numeric fields come back as strings. Cast IDs to integers, but handle monetary amounts as decimal strings with bcmath or brick/math. Never use floats for invoice totals or payments.

  • Dates are WHMCS-local strings. WHMCS returns dates in its configured system timezone and format. Parse them as DateTimeImmutable in the WHMCS timezone, then convert to UTC for storage.

  • Response shapes are inconsistent. GetClients returns a clients list, GetClientsDetails returns nested client/stats/customfields, and some actions return ID-keyed maps rather than arrays. Normalize in your adapter and test empty results, which may be [] or an empty object.

  • Do not parse WHMCS error messages. They are human-readable and change between versions. Log them, but map failures to your own error codes based on the action and context.

  • Pagination defaults are small. Always pass limitnum, always handle totalresults, and use a conservative page size. Large pages can hit WHMCS's PHP execution limit and return a 500 or truncated response.

  • GetClientsDetails needs clientid or email. Calling it with neither is a common first-call mistake.

  • Write actions are not idempotent. AddOrder, AddClient, AddInvoicePayment, OpenTicket, and AddTicketReply can duplicate records on retry. Record intent before the call, retry only transport failures, and reconcile timeouts manually.

  • Adapter discovery failures look like package bugs. "No HTTP client found" or "No PSR-7 implementation" means you have not installed the adapter the package expects. Check composer.json for php-http/discovery, PSR-18, or HTTPlug, and install the matching implementation.

  • Set timeouts on the underlying HTTP client. WHMCS reports and order creation can be slow. Use something like a 10-second connect timeout and 60-second read timeout, then retry with backoff at the job level.

  • Debug with middleware, not var_dump. Add a logging middleware/plugin to the injected HTTP client to capture method, URL, sanitized body, status, and response body. Also check Utilities > Logs > API Log in WHMCS to see the action and parameters that actually arrived.

  • Pin the package and keep it behind an interface. This is a small, low-visibility package; pin a known-good version and avoid coupling application code directly to its classes. A thin repository layer means you can replace it with a direct Guzzle/PSR-18 call if maintenance becomes a problem.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager