This package wires a Laravel app to a WHMCS installation in two directions: it calls the WHMCS API (client lookups, orders, invoices, service actions) and it authenticates users against WHMCS, including handing them off into the WHMCS client area via SSO. Treat it as your billing-system adapter — you should almost never write raw curl against WHMCS again.
1. Install and publish config
composer require <vendor>/<package> # v2.0.0
php artisan vendor:publish --tag=whmcs-config
2. Set env
WHMCS_URL=https://billing.example.com
WHMCS_IDENTIFIER=your-api-identifier
WHMCS_SECRET=your-api-secret
WHMCS_TIMEOUT=15
WHMCS_SSL_VERIFY=true
WHMCS_URL now defaults to https:// and SSL verification is on by default in v2 — do not disable it to "make it work"; fix the certificate instead.
3. Migrate the session key first
If you're upgrading from v1.x, the authenticated session key changed from user to whmcs_user. Anything reading session('user'), or Blade using Auth::user() alongside a legacy session read, needs a pass before you deploy. This is the single most likely silent breakage.
4. First use case: log a client in
Register the package's guard/provider, then in your login controller authenticate against WHMCS and provide a "Go to billing" link:
public function login(Request $request, Whmcs $whmcs)
{
// package handles the WHMCS API login + local user sync
// then, anywhere in your app:
return redirect($whmcs->getSsoUrl($clientId));
}
getSsoUrl() / redirectSso() use WHMCS's CreateSsoToken flow, which is the supported path on WHMCS 8.1+. If you find getAutoLoginUrl() or redirectAutoLogin() anywhere in the codebase, they still work but emit E_USER_DEPRECATED — schedule the rename rather than waiting for v3.
Where to look first: config/whmcs.php (URL, credentials, timeout, SSL, session key), then the service provider to see which guard and user provider are registered. That tells you how authentication is actually resolved in your app.
Wrap API calls behind your own service. Don't sprinkle sbGetClients() calls through controllers. Put a thin BillingService in front so you can cache, log, and fake it in tests.
class BillingService
{
public function __construct(private Whmcs $whmcs) {}
public function activeClients(): Collection
{
return Cache::remember('whmcs.clients.active', now()->addMinutes(10), fn () =>
collect($this->whmcs->sbGetClients([
'status' => 'Active',
'orderby' => 'firstname',
'limitnum' => 250,
])['clients']['client'] ?? [])
);
}
}
The new $orderby and $status parameters on sbGetClients() (WHMCS 8.2+) let you push filtering and sorting server-side. Use them — but guard for WHMCS < 8.2 in environments that haven't upgraded, since the parameters are ignored or rejected depending on version.
SSO hand-off is a two-step flow, not a redirect. createSsoToken() returns the token; getSsoUrl() builds the destination; redirectSso() does both and returns a redirect response. Use the explicit form when you need to log or store the token before redirecting (common for audit trails and for links emailed to customers):
$token = $whmcs->createSsoToken($clientId);
Log::info('whmcs.sso', ['client' => $clientId, 'user' => auth()->id()]);
return redirect($whmcs->getSsoUrl($token));
Treat the SSO URL as short-lived. Build it at click time, never store it in a queue payload or a cached view fragment. If you generate it during page render and the user sits on the page, the token expires and the redirect lands on a WHMCS error page — which users will report as "billing is broken."
Authentication / user provider. v2 implements rehashPasswordIfRequired() on the UserProvider contract (required by Laravel 11+). That means the standard auth pipeline works end to end: Auth::attempt(), password rehashing after a WHMCS-side algorithm change, and Auth::user() resolving your local Eloquent user. Keep your local users table as the source for app-side relationships; use the WHMCS client ID as the join key.
// migration
$table->unsignedBigInteger('whmcs_client_id')->nullable()->unique();
Use the Http facade's testing hooks. Because v2 replaced Guzzle with Illuminate\Support\Facades\Http, you can fake the entire WHMCS API in feature tests — this is a major quality-of-life win over v1:
Http::fake([
'billing.example.com/api/*' => Http::response([
'result' => 'success',
'clients' => ['client' => [['id' => 1, 'firstname' => 'Ada']]],
]),
]);
Note the result => 'success' envelope — WHMCS returns HTTP 200 even for API-level errors, so always assert on the payload, not just the status code.
Queue anything write-heavy. Provisioning, suspending, and invoice-generating calls belong in jobs with retryUntil/backoff. WHMCS API calls can be slow, and the new WHMCS_TIMEOUT knob is your circuit breaker. Pair a shorter timeout with a job retry policy rather than a long timeout with no retries.
Local testing. Point WHMCS_URL at a dev WHMCS instance and keep credentials out of .env.example. If you can't run WHMCS locally, wrap all outbound calls in the service class above and fake it — that's the only way to get deterministic feature tests.
Breaking-change checklist for v2. Work through these before merging the upgrade:
composer.json.user → whmcs_user. Grep for the old key and for any custom guard config that hardcoded it.https://. If WHMCS_URL in an env file lacks a scheme, behavior changed — verify what your config actually resolves to.getAutoLoginUrl() / redirectAutoLogin() are deprecated, not removed. They trigger E_USER_DEPRECATED. If your error handler converts deprecations to exceptions (common in strict CI setups), tests will fail loudly — which is good, but it will look like a package bug. Rename to getSsoUrl() / redirectSso() and move on.
WHMCS version gating matters. SSO requires WHMCS 8.1+ (AutoAuth was removed there). The sbGetClients() $orderby/$status params require 8.2+. If you support customers on mixed WHMCS versions, feature-detect rather than assuming; a version check against a cached Whmcs details call is cheap.
API "errors" arrive as HTTP 200. A response of {"result":"error","message":"..."} is not a transport failure. Don't rely on Http::fake returning 500 to simulate an API problem — fake the error envelope, and make sure your service throws a domain exception rather than silently returning an empty collection. Silent empty collections are the number-one cause of "the client list is blank" tickets.
Debugging outbound calls. Http::fake() in tests, but in dev use Http::globalRequestMiddleware() or a Log::debug in your service wrapper to dump the action name and payload. Never log identifier/secret or SSO tokens — they're credentials. If a call fails with an unhelpful message, check the action name spelling first; WHMCS returns a generic error for unknown actions.
Timeout tuning. WHMCS_TIMEOUT is a single global value. If GetClients-style bulk reads need more headroom than CreateSsoToken-style interactive calls, keep the env default low (10–15s) for user-facing paths and override per call in your service for batch jobs.
Extension points. The service provider binds the client and the user provider — override them in AppServiceProvider (or a dedicated BillingServiceProvider registered after the package) rather than editing vendor config. Custom guards, custom user resolution, and a custom HTTP client (for retries/telemetry) all hang off those bindings.
Version-pin your config expectations. The default URL scheme and session key both changed in one release. Anyone with a stale published config/whmcs.php will not receive these changes (published configs aren't overwritten) — diff your published config against the package's current one as part of the upgrade, or you'll get inconsistent behavior between environments.
How can I help you explore Laravel packages today?