Client SDK

v1 JSON over HTTPS Bearer credentials or HMAC signatures

licensify/client-sdk is a dependency-free PHP package that turns licensing into a single call.

Install

The SDK lives in its own repository — syedshaishad/licensify-client-sdk — and is installed from there. Add the repository to your composer.json:

{
    "repositories": [
        {
            "type": "git",
            "url": "https://github.com/syedshaishad/licensify-client-sdk.git"
        }
    ]
}

A git entry rather than a vcs one on purpose: vcs asks the GitHub API for the repository's details first, and the API answers unauthenticated requests only sixty times an hour. Behind a shared address that runs out, and the failure is a confusing message about SSH credentials. A git entry clones over HTTPS and never touches the API.

Then:

composer require licensify/client-sdk:1.0.0

Requires PHP 8.3+ with ext-sodium and ext-json. The package has no runtime dependencies, so nothing else is pulled in.

Configure

LICENSE_SERVER_URL=https://license.example.com
LICENSE_PRODUCT_CODE=MYCRM
LICENSE_KEY=XXXX-XXXX-XXXX-XXXX-XXXX
LICENSE_API_KEY=lk_a1b2c3d4e5
LICENSE_API_SECRET=your-api-secret

The SDK derives everything else by itself: it generates and persists an installation id, detects the current domain, and reads the running version.

Use

use Licensify\Client\License;

$status = License::validate();

if (! $status->valid()) {
    abort(403, $status->message());
}

In a route middleware:

use Licensify\Client\Laravel\Http\Middleware\EnsureLicenseIsValid;

// routes/web.php
Route::middleware(EnsureLicenseIsValid::class)->group(function () {
    // ...
});

Other calls:

License::activate();               // register this installation
License::deactivate('new server'); // release the slot
License::status();                 // licence state, no installation
License::updateAvailable();        // ?UpdateInfo
License::valid();                  // bool
License::certificate();            // the cached token, for inspection
License::graceExpiresAt();         // ?DateTimeImmutable
License::expireCache();            // force re-validation

Integrating in four steps

  1. Create the product in the dashboard.
  2. Add the repository to composer.json and composer require licensify/client-sdk:1.0.0.
  3. Add the four environment variables.
  4. Call License::validate() once, in a middleware.

No change to your controllers, models or routes.

How offline validation works

Every successful validation returns a certificate: a compact token signed with the server's Ed25519 private key.

  1. The SDK verifies the signature against the public keys it trusts.
  2. It checks nbf and exp on its own clock.
  3. It records the certificate's rev counter.
  4. While the certificate is valid it answers without contacting the server.

When the server is unreachable the SDK falls back to the cached certificate for the remainder of the grace window. When that window closes it fails closed.

Why deleting the cache does not help an attacker

  • The client can only verify certificates, never mint one. The private key never leaves the server.
  • Deleting the cache forces an immediate re-validation. Offline, that fails.
  • The rev counter is bumped whenever a license is suspended, revoked or changed, so a stale cached certificate is rejected at the next contact.
  • Combined with the grace window, a compromised installation is bounded to the configured offline period — and that period is set on the server.

The SDK never treats "I could not reach the server" as "valid".

Key rotation

Certificates name the key that signed them in a kid header. When the server rotates:

  1. It promotes a new key and keeps the previous one published.
  2. SDKs fetch the current keyring from GET /api/v1/products/{code}/public-key.
  3. A certificate signed by a still-published retired key keeps verifying.

Recover from UNKNOWN_SIGNING_KEY by refreshing the keyring:

License::refreshKeyring();

No new SDK release is needed.

Framework support

The core is plain PHP and works anywhere — Laravel, CodeIgniter, a plain script. A Laravel-specific bridge ships separately:

// config/license.php
return [
    'server_url'      => env('LICENSE_SERVER_URL'),
    'product_code'    => env('LICENSE_PRODUCT_CODE'),
    'license_key'     => env('LICENSE_KEY'),
    'api_key'         => env('LICENSE_API_KEY'),
    'api_secret'      => env('LICENSE_API_SECRET'),
    'timeout'         => 10,
    'verify_ssl'      => true,
    'validate_on'     => ['request'],
];

The bridge registers a service provider, a License facade, the LicenseManager binding and the license:check Artisan command:

php artisan license:check

Testing against a local server

License::expireCache();
License::activate();          // must be reachable
License::validate();          // true
License::deactivate('test');

Because the SDK is fail-closed by design, a test suite that blocks network access will correctly report an invalid license. Inject a fake transport, or point LICENSE_SERVER_URL at a running instance.

Page 7 of 8