labs64/netlicensingclient-php
A PHP SDK for the Labs64 NetLicensing API, letting you manage products, licenses, customers, and entitlements programmatically. Easily add flexible licensing and entitlement checks to any PHP application.
Install the client (it is a plain library — no Laravel service provider or facade is auto-discovered):
composer require labs64/netlicensingclient
Before writing code, create the objects you will reference from PHP in the NetLicensing UI: a product (with its number), one or more license templates (the actual licensing models/modules), and an API key under your account. Those numbers are configuration, not something the client generates for you.
Read these classes first:
NetLicensing\Context — credentials, base URL, security modeNetLicensing\Client — every API operation (getProduct, createLicensee, validate, createToken, …)NetLicensing\ValidationParameters / ValidationResult — the hot path for entitlement checksNetLicensing\Product, Licensee, License, LicenseTemplate, Token, PaymentMethod, Transaction — thin attribute bagsThe first useful call is a server-side validation for an existing licensee:
use NetLicensing\Context;
use NetLicensing\Client;
use NetLicensing\ValidationParameters;
$context = new Context(
config('services.netlicensing.username'),
config('services.netlicensing.password'),
null,
Context::DEFAULT_BASE_URL,
Context::BASIC_AUTHENTICATION
);
$client = new Client($context);
$params = new ValidationParameters();
$params->setProductNumber(config('services.netlicensing.product_number'));
$params->setLicenseeNumber($user->netlicensing_licensee_number);
$result = $client->validate($params);
if ($result->getValid() === 'true') {
// entitlement granted
}
For production use API-key auth instead of a human login (Context::API_KEY, empty username/password). Note the === 'true' — valid comes back as a string (see Gotchas).
Bind the client as a container singleton. Keep credentials in config/services.php and build one Context/Client per process:
// AppServiceProvider::register()
$this->app->singleton(Client::class, function () {
$context = new Context();
$context->setApiKey(config('services.netlicensing.api_key'));
$context->setBaseUrl(config('services.netlicensing.base_url'));
$context->setSecurityMode(Context::API_KEY);
return new Client($context);
});
Constructor-argument and setter styles have both existed across majors; use whichever your installed tag exposes.
Provision a licensee at registration. Create the licensee once, store its number on your users table, and use it for every later call. Do not use the email as the number:
$licensee = new NetLicensing\Licensee();
$licensee->setProductNumber(config('services.netlicensing.product_number'));
$licensee->setName($user->email);
$licensee->setActive('true');
$licensee = $client->createLicensee($licensee);
$user->update(['netlicensing_licensee_number' => $licensee->getNumber()]);
Attach licenses through templates. A licensee alone grants nothing until a license referencing a license template is created:
$license = new NetLicensing\License();
$license->setLicenseeNumber($licenseeNumber);
$license->setLicenseTemplateNumber($templateNumber);
$license->setActive('true');
$client->createLicense($license);
Wrap the client behind a small gateway. The client is concrete and network-bound, so hide it behind an interface you can fake in tests and cache in front of:
interface LicenseGateway
{
public function isValid(string $licenseeNumber, ?string $module = null): bool;
}
Implementation with caching:
public function isValid(string $licenseeNumber, ?string $module = null): bool
{
$key = "netlicensing:{$licenseeNumber}:".($module ?? 'all');
return Cache::remember($key, now()->addMinutes(15), function () use ($licenseeNumber, $module) {
$params = new ValidationParameters();
$params->setProductNumber(config('services.netlicensing.product_number'));
$params->setLicenseeNumber($licenseeNumber);
if ($module) {
$params->setProductModuleNumber($module);
}
return $this->client->validate($params)->getValid() === 'true';
});
}
Use it in middleware or a Gate. Check the gateway in a EnsureLicense middleware for premium routes, or in a Gate ability so Blade/controllers can use @can. Keep the call site boolean — don't leak ValidationResult into controllers.
Token-based validation for devices. For desktop/mobile/IoT clients that cannot hold your API key, issue a token server-side and have the device validate anonymously:
$token = new NetLicensing\Token();
$token->setTokenType('APIKEY');
$token->setProductNumber($productNumber);
$token->setLicenseeNumber($licenseeNumber);
$token->setActive('true');
$token = $client->createToken($token);
return $token->getToken(); // treat as a secret
The device then validates with Context::ANONYMOUS and ValidationParameters::setToken(...).
Payment/transaction flows. For paid licenses, the pattern is: create a PaymentMethod for the licensee, create a Transaction referencing it and a source, then create the License with the transaction number. Drive this from a queued job or a webhook handler so the checkout request doesn't block on two or three API round trips.
Testing. Bind a fake LicenseGateway in the test container rather than mocking the concrete client:
$this->app->instance(LicenseGateway::class, new FakeLicenseGateway(valid: true));
If you must test the wrapper itself, inject a Guzzle MockHandler into a Client built for tests and assert that entity attributes are serialized correctly.
getValid() returns the string 'true'/'false'. if ($result->getValid()) is always true. Always compare === 'true' or cast deliberately.active as 'true'/'false'; the same applies to other boolean attributes. Passing real booleans works in some versions and silently serializes wrong in others.number vs licenseeNumber/productNumber. Product and Licensee use setNumber(); child entities (License, Token, PaymentMethod) reference parents via setLicenseeNumber()/setProductNumber(). Mixing these up produces 400 Entity reference not found.infos. Guzzle throws ClientException/ServerException on 4xx/5xx, while the wrapper may throw NetLicensingException. In both cases the useful reason is in the response body's infos array — decode it before logging, or your logs will just say "400 Bad Request".active=false (or updateLicense) instead of deleting, especially for churn.Context/Client doesn't let you inject a Guzzle client in your version, keep validation out of latency-critical paths or wrap it with your own timeout/retry guard.composer why-not labs64/netlicensingclient <version> to see the conflict./core/v2/rest endpoint. A trailing segment missing here yields 404s that look like auth failures.Carbon::parse() and compare in UTC; don't assume the app timezone.How can I help you explore Laravel packages today?