Integrate the unchanged PHP SDK with an explicit validity gate and a stable private installation state.
What you will learn
- Install the actual source package and configure every constructor option.
- Use the complete session CLI and distinguish release success from feature access.
- Understand every exposed member, storage/error/offline boundary and packaging limit.
Prerequisites
- Complete the Sandbox quickstart.
- Read signed responses and use only synthetic Sandbox data.
Runtime and package
PHP 8.2+ for the actual readonly result class; cURL/JSON and native Sodium or Composer-loaded sodium_compat. composer.json still declares ^8.1; this guide does not change that mismatch. Clean install uses PHP8.5.4 and sodium_compat2.5.2.
Package kdpika/license-sdk, current label 1.0.0. Download kdpika-php-sdk-1.0.0.zip from the SDK overview and verify SHA-256 before extracting. This is a local source install, not a claim that a public registry release exists.
Install and use
Run the installation command in an empty application project with the extracted SDK directory. Set KDP_APP_ID and a private KDP_STATE_DIR. For PHP also set KDP_SDK_DIR to the extracted package directory. For activation set KDP_LICENSE_KEY and optional KDP_CUSTOMER_EMAIL from trusted user input/your secret environment, not source literals.
composer install --working-dir ./kdpika-php-sdk --no-interaction --no-plugins --no-scripts --prefer-dist
Expected: the exact local SDK installs/builds and its imports resolve. Python on Windows uses .venv\Scripts\python.exe instead of .venv/bin/python; the transcript is the tested macOS path. No registry release is implied by dependency restoration.
The session program is complete and runs one explicit action per invocation, preserving state across processes. Its SDK constructor deliberately sets SANDBOX. The key-prefix/URL checks are demonstration guards, not a replacement for server environment lookup or SDK verification. HTTPS is the default; local fixture HTTP requires KDP_ALLOW_LOOPBACK=1.
<?php
declare(strict_types=1);
// Sandbox session CLI: PHP8.2+, cURL, Composer-installed official SDK1.0.0.
// Run php session.php status|activate|check|validate|deactivate.
// Set KDP_APP_ID,KDP_STATE_DIR,KDP_SDK_DIR (extracted package path).
// Activation also uses KDP_LICENSE_KEY and optional KDP_CUSTOMER_EMAIL.
// Expected safe JSON; no raw payload/token. Never skip the validity guard.
use KDPika\LicenseSdk\LicenseClient;
function required(string $name):string{
$value=trim((string)getenv($name));if($value==='')throw new RuntimeException('Missing configuration');return $value;
}
try{
$action=$argc===2?$argv[1]:'';
if(!in_array($action,['status','activate','check','validate','deactivate'],true))throw new RuntimeException('Choose an action');
$base=rtrim(getenv('KDP_BASE_URL')?:'https://licensing.kdpika.com','/');$uri=parse_url($base);
$local=is_array($uri)&&($uri['scheme']??'')==='http'&&in_array($uri['host']??'',['127.0.0.1','localhost'],true)&&getenv('KDP_ALLOW_LOOPBACK')==='1';
if(!is_array($uri)||!isset($uri['host'])||isset($uri['user'])||isset($uri['pass'])||isset($uri['path'])||isset($uri['query'])||isset($uri['fragment'])||(($uri['scheme']??'')!=='https'&&!$local))throw new RuntimeException('Use an HTTPS site origin');
require required('KDP_SDK_DIR').'/vendor/autoload.php';
$client=new LicenseClient(product:required('KDP_APP_ID'),environment:'SANDBOX',baseUrl:$base,
applicationName:'KDPika SDK session example',applicationVersion:'1.0.0',storageDir:required('KDP_STATE_DIR'));
if($action==='status'){
echo json_encode(['activated'=>$client->activated(),'storage_security'=>'permission-protected-file',
'installation_hash'=>hash('sha256',$client->installationId())],JSON_THROW_ON_ERROR),PHP_EOL;exit;
}
if($action==='activate'){
$key=required('KDP_LICENSE_KEY');if(!preg_match('/^TEST[A-Z0-9]*-/i',$key))throw new RuntimeException('Use a Sandbox key');
$result=$client->activate($key,getenv('KDP_CUSTOMER_EMAIL')?:null);
}else $result=$client->$action();
$active=$action!=='deactivate'&&$result->valid&&$result->signatureValid;
$limit=$active?$result->featureValue('seat_count'):null;
echo json_encode(['valid'=>$result->valid,'source'=>$result->source,'error_code'=>$result->errorCode,
'signature_valid'=>$result->signatureValid,'pdf_export_allowed'=>$active&&$result->feature('pdf_export'),
'seat_count'=>is_int($limit)&&$limit>=0?$limit:null,
'request_id'=>preg_match('/^req_[a-f0-9]{24}$/D',$result->requestId??'')?$result->requestId:null,
'message'=>$action==='deactivate'&&$result->valid?'This installation is deactivated.':($result->valid?'Your license is ready.':'Your license could not be confirmed. Check your key or contact the software publisher.')],JSON_THROW_ON_ERROR),PHP_EOL;
exit($result->valid?0:1);
}catch(Throwable){
fwrite(STDERR,'License check unavailable. Review configuration, connectivity and private storage permissions. Do not disable signature verification.'.PHP_EOL);exit(2);
}
- Run php session.php status. Expected: activated=false for fresh storage, with only an installation hash and storage category.
- Run php session.php activate with a Sandbox key and matching email. Expected: valid=true/signature_valid=true or an explicit refusal, never a raw token/payload dump.
- Run php session.php check using the same state directory. Expected: current valid result from the eligible cache or online path; no feature is enabled on failure.
- Run php session.php validate. Expected: forced-online path, subject to the documented bounded transport-failure fallback.
- Run php session.php deactivate. Expected: the installation seat is released and local token/cache removed; pdf_export_allowed remains false for this release operation.
- Run php session.php status again. Expected: activated=false with the same installation hash. Cumulative activation_count is not reset.
The programs exit0 for an accepted operation/status, exit1 for a licensing refusal, exit2 for configuration/unexpected failure. They expose safe decisions only. seat_count is read only from a verified valid result and must be a nonnegative integer; an absent value remains null. Result feature helpers cannot substitute for the validity/signature gate.
Complete constructor configuration
| Option | Type | Default | Meaning |
|---|---|---|---|
| product | string | required | Public App ID; never the internal product ID or an lmp_ key. |
| environment | string | LIVE (Python: production maps to LIVE) | Set SANDBOX explicitly for examples. production/test aliases normalize to LIVE/SANDBOX. |
| baseUrl | string | https://licensing.kdpika.com | Site origin, without /api/v1; default HTTPS. Loopback HTTP is for local testing only. |
| applicationName | string | Licensed Application | Names the default application storage directory and device fallback label. |
| applicationVersion | string | empty string | Application release sent at activation. Empty version skips the server version-range check. |
| verificationKey | ?array | null/None | Optional pinned key with key_id and raw base64url public_key; manual rotation. |
| storageDir | ?string | OS-derived application storage root | Namespace includes origin, product and environment; keep outside public web roots. |
| timeout | float seconds | 15 seconds / 15000 milliseconds | Transport timeout, not an offline grace or validation interval. |
| transport | ?callable | null (built-in cURL) | Test injection must preserve the actual response/error contract; never accept unsigned fixtures in release. |
| clock | ?callable | current UTC-compatible time | Testing clock; do not expose time control to untrusted application callers. |
Timeout is an HTTP timeout, not a client-side way to configure offline grace or validation interval. Those times come from the verified signed payload and the product configuration. Defaults are actual source defaults; the runnable sample explicitly changes environment/storage choices for safe isolated tests.
Public members and result contract
This inventory includes constructors, fields/properties, lifecycle/result helpers and exposed low-level members. JavaScript helpers are language-public even when they are implementation details. Python non-underscore storage/transport classes are listed rather than silently hidden. The exact source signature is shown so a guide cannot invent a method such as has_feature().
| Member | Kind | Exact signature / field | Use and return/error boundary |
|---|---|---|---|
| LicenseClient.__construct | constructor | public function __construct( private readonly string $product, string $environment = 'LIVE', string $baseUrl = 'https://licensing.kdpika.com', private readonly string $applicationName = 'Licensed Application', private readonly string $applicationVersion = '', ?array $verificationKey = null, ?string $storageDir = null, private readonly float $timeout = 15.0, ?callable $transport = null, ?callable $clock = null, ) | Configure this type using the exact signature. Client construction creates/loads its local installation namespace; result construction alone is not verification. |
| LicenseClient.installationId | method | public function installationId(): string | Persisted random installation identity; not a hardware fingerprint or secret developer API key. |
| LicenseClient.activated | method | public function activated(): bool | Token-presence status only; not a current entitlement decision. |
| LicenseClient.activate | method | public function activate(string $licenseKey, ?string $email = null): LicenseResult | Send activation; return a valid signed result or explicit refusal. Success stores the token and signed state; verification failure attempts best-effort deactivation. |
| LicenseClient.check | method | public function check(bool $forceOnline = false): LicenseResult | Use verified cache when eligible or validate online. A transport failure can enter bounded offline policy; explicit server rejection is not offline permission. |
| LicenseClient.validate | method | public function validate(): LicenseResult | Force the check path online; transport failure can still attempt bounded verified offline fallback, not an unconditional online-only guarantee. |
| LicenseClient.deactivate | method | public function deactivate(): LicenseResult | Release this token seat online and clear token/cache on success. A successful release result does not grant access. Installation identity remains. |
| LicenseClient.feature | method | public function feature(string $name, bool $default = false): bool | Client helper may perform a new check but does not enforce valid itself. Prefer one accepted result, then result feature access. Never use the helper as the sole permission gate. |
| LicenseResult.__construct | constructor | public function __construct( public bool $valid, public string $source, public array $payload = [], public ?string $errorCode = null, public string $message = '', public ?string $requestId = null, public bool $signatureValid = false, public string $storageSecurity = 'permission-protected-file', ) | Configure this type using the exact signature. Client construction creates/loads its local installation namespace; result construction alone is not verification. |
| LicenseResult.feature | method | public function feature(string $name, bool $default = false): bool | Boolean-style feature lookup only. Never authorizes invalid results; C# true/false fallback semantics differ from truthiness in the other SDKs. |
| LicenseResult.featureValue | method | public function featureValue(string $name, mixed $default = null): mixed | Read the typed feature value/default. Check valid and the actual feature type first. |
| LicenseResult.valid | field | public bool $valid | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.source | field | public string $source | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.payload | field | public array $payload | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.errorCode | field | public ?string $errorCode | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.message | field | public string $message | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.requestId | field | public ?string $requestId | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.signatureValid | field | public bool $signatureValid | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.storageSecurity | field | public string $storageSecurity | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
A LicenseResult carries valid/source/payload/error code/message/request ID/signature status/storage category under the language-specific spelling above. Lifecycle calls return this result type. Unknown codes fail closed. HttpResponse/custom transport status is not an access grant. A field in a constructed result has not been verified merely because it is present.
The exact runnable session covers the real Sandbox lifecycle and safe output. Representative local/model/helper assertions execute every inventoried public member; the optional Python dialog has four genuine offscreen Qt cases. These are not claims that every branch, injected cancellation, native vault, framework or target OS was executed. The full app examples record actual packaged/native versus cross-build/controller boundaries explicitly.
Runnable member examples
This separate complete program exercises the public local/result/helper surface with explicit assertions and no access grant. It is an isolated conformance exercise, not an alternative licensing implementation. Run it beside the installed source package using php members.php. Expected: result=PASS and a list of exercised members, without credentials or raw customer payloads.
Python makes one read-only public-key GET to KDP_BASE_URL (default HTTPS production origin); its product is an unusable example App ID, and it does not activate. Its optional PyQt dialog is tested separately in four offscreen cases. PHP and C# use only synthetic local objects and no HTTP. JavaScript uses an ephemeral test signing key and injected synthetic transport, never a real KDPika credential or network response. The normal session example above is separately tested against the real isolated Sandbox.
These examples use their own fresh temporary directories. They never inspect or delete an existing installation. Result fields are constructed as invalid deliberately: a true feature helper cannot override valid=false. Typed return assertions demonstrate behavior, not complete branch, native vault or target-OS coverage.
<?php
declare(strict_types=1);
// Local model/member exercise; no real token, network request or access grant.
// KDP_SDK_DIR points to the extracted SDK with its Composer vendor directory.
use KDPika\LicenseSdk\LicenseClient;
use KDPika\LicenseSdk\LicenseResult;
$sdk = getenv('KDP_SDK_DIR');
if (!$sdk || !is_file($sdk . '/vendor/autoload.php')) {
fwrite(STDERR, "Set KDP_SDK_DIR to the installed source package.\n"); exit(2);
}
require $sdk . '/vendor/autoload.php';
$covered = [];
$check = static function (string $member, bool $condition) use (&$covered): void {
if (!$condition) throw new RuntimeException('Member example failed: ' . $member);
$covered[] = $member;
};
$root = sys_get_temp_dir() . '/kdpika-members-' . bin2hex(random_bytes(12));
if (!mkdir($root, 0700)) throw new RuntimeException('Private scratch directory unavailable.');
$client = new LicenseClient('YOUR_APP_ID', 'SANDBOX', storageDir: $root);
$check('LicenseClient.__construct', $client instanceof LicenseClient);
$check('LicenseClient.installationId', str_starts_with($client->installationId(), 'lmp-install-'));
$check('LicenseClient.activated', !$client->activated());
$check('LicenseClient.activate', $client->activate('')->errorCode === 'INVALID_LICENSE');
$check('LicenseClient.check', $client->check()->errorCode === 'NOT_ACTIVATED');
$check('LicenseClient.validate', $client->validate()->errorCode === 'NOT_ACTIVATED');
$check('LicenseClient.deactivate', $client->deactivate()->errorCode === 'NOT_ACTIVATED');
$check('LicenseClient.feature', $client->feature('pdf_export') === false);
// A constructed result is NOT a signature verification or license decision.
$payload = ['features' => ['pdf_export' => true, 'seat_count' => 2]];
$result = new LicenseResult(false, 'example', $payload, 'EXAMPLE_ONLY', 'Example', 'req_example', false, 'unknown');
$check('LicenseResult.__construct', $result instanceof LicenseResult);
$expected = ['valid' => false, 'source' => 'example', 'payload' => $payload, 'errorCode' => 'EXAMPLE_ONLY',
'message' => 'Example', 'requestId' => 'req_example', 'signatureValid' => false, 'storageSecurity' => 'unknown'];
foreach ($expected as $name => $value) $check('LicenseResult.' . $name, $result->$name === $value);
$check('LicenseResult.feature', $result->feature('pdf_export') && !$result->feature('missing'));
$check('LicenseResult.featureValue', $result->featureValue('seat_count') === 2 && $result->featureValue('missing', 7) === 7);
$check('application.fail_closed', !($result->valid && $result->signatureValid && $result->feature('pdf_export')));
// Only this newly created scratch directory is cleaned; no user state is read.
foreach (new DirectoryIterator($root) as $directory) {
if ($directory->isDot()) continue;
foreach (new DirectoryIterator($directory->getPathname()) as $file) {
if (!$file->isDot()) unlink($file->getPathname());
}
rmdir($directory->getPathname());
}
rmdir($root);
echo json_encode(['result' => 'PASS', 'members' => $covered, 'mode' => 'synthetic local objects; no HTTP', 'access_granted' => false], JSON_THROW_ON_ERROR) . PHP_EOL;
Errors and exceptions
Configuration/storage/transport/signature helpers use RuntimeException; normal online/local licensing errors return LicenseResult. A malformed custom transport or filesystem failure can escape a method. Wrap the application boundary, log only safe operation/time/code/request ID, and keep display_errors off on production. PHP8.5 curl_close deprecation is a known compatibility notice, not a signature bypass.
Use the complete error table for code/namespace/cause/correction and safe user messages. Log only application version, UTC time, action, code, source and a validated req_ correlation value. Never log the key, token, Authorization header, customer payload or an unredacted state file. No debug flag changes the signature or validity policy.
Storage and installation identity
Permission-protected files: a namespaced0700 directory and atomic0600 token/cache/identity files where the OS supports those permissions. No automatic keyring/DPAPI/Keychain adapter is implemented. Do not put storageDir under public_html or share it between tenants. activated() means a token exists, not that the license is currently valid.
Storage namespace is based on origin/product/environment. Keep those stable across app updates; changing them can create a separate identity/seat. Reinstalling or deleting identity can consume another cumulative activation even after a seat was released. Do not use machine serials/MAC addresses as a substitute for the persistent random ID.
Offline and clock policy
A new activation requires network. check uses verified signed state before next_validation_at when eligible. After that point it validates online; only a transport failure attempts the verified offline path. Explicit server rejection clears signed cache and selected invalid-token/device/revocation/expiry/cancellation errors also remove the stored credential. Offline/cache paths require active status, valid signature, product/environment/installation binding, expiry/grace and clock policy.
Clock rollback tolerance is300 seconds behind the latest trusted checkpoint/server time. A user controlling the host can patch the program or roll back a full machine snapshot; local checks are not absolute anti-tamper guarantees. Some malformed/missing signed time cases and current canonicalization issues are recorded limitations, not intended feature grants.
Validation interval/offline grace are not constructor options. Configure the product on KDPika, then verify what the signed server_time/next_validation_at/offline_grace_until actually say. Offline grace is based on server_time, not added after next_validation_at. Do not calculate a second extension or treat a failed check as permission.
Read the full offline guide and test vectors. Known Python separator/exponent, JavaScript integral-float and C# cached-Unicode compatibility failures remain unchanged. A successful narrow local lifecycle is not universal canonicalization conformance.
Threading, asynchronous work and lifetime
The API is synchronous; no threading, event-loop or interprocess serialization is promised. Serialize activate/deactivate/identity writes for the same installation. On a shared backend, model your application tenant/installation explicitly instead of sharing one cache/token across all buyers.
Application and framework integration
Laravel/Symfony can use a Composer path repository or a private source package for this same code, but a public Packagist release is not established. Create the client in your application service layer, not a public unauthenticated controller with arbitrary product/storage input. WordPress integrations must keep credentials/cache outside web-accessible paths, protect actions with actual authentication/authorization/CSRF controls and retain the same stable installation identity. No framework plugin is implied by this library.
Packaging and distribution
Deploy the exact SDK source and Composer dependencies with ext-curl and Sodium capability. Keep vendor/server state private, lock dependency versions in your application and verify permissions under the actual PHP process user. Do not install a Node/Docker toolchain on shared hosting for this SDK. Native Sodium and compatibility fallback behavior need separate tests; a successful native run is not proof of fallback execution.
Upgrade and uninstall
Pin the current source checksum and runtime/dependency lock. Before changing a version, compare constructor/public members, storage namespace, canonical bytes, time policy and errors. Run activation/cache reload/offline/explicit refusal/deactivation on a separate Sandbox installation. No historical version or compatibility window is invented by this guide.
Deactivate while online before intentionally removing a deployed installation. Removing the package alone does not release a server seat or erase your application storage. Retaining storage may preserve identity for a later reinstall; deleting it can create another installation. Follow your application privacy/retention policy rather than recursively deleting a broad user directory.
The language-specific removal command applies only if the package/reference was installed into that application project:
composer remove kdpika/license-sdk
For a direct copied PHP source directory, removing a Composer root package is not the same as composer remove in a consuming application. Remove only the exact unused reference/files after backing up required application state; no broad deletion command is prescribed.
Expected result
Your application handles explicit refusal without enabling features, preserves the installation identity, and deactivates deliberately when releasing a seat. Check the real signed result, not only a transport status.
Common mistakes
- Using activated/token presence or a feature helper as a validity proof.
- Putting /api/v1 in the SDK site-origin constructor or omitting SANDBOX in a test.
- Deleting identity/cache and assuming a server seat or cumulative activation was reset.
- Suppressing a signature mismatch instead of testing the actual release limitation.
Security notes
Keep credentials off renderer/browser surfaces, retain TLS/signature verification, and use a verified valid result for the specific access action. A successful deactivate/status operation never grants a feature. Use private storage and safe correlation metadata only.
Troubleshooting
| Symptom | Cause | Action |
|---|---|---|
| NOT_ACTIVATED | No stored credential in this namespace | Activate a Sandbox installation; keep origin/product/environment/storage stable |
| INVALID_LICENSE | Wrong key/email/App ID/environment | Correct the input; do not retry unchanged requests blindly |
| SIGNATURE_INVALID or cache rejection | Tampering, trust mismatch or current canonicalization limitation | Fail closed and run real response/cache/vector checks |
| State disappeared after update | Application name/origin/storage namespace changed | Restore the intended stable namespace; reconcile any extra seat |
| Network failure at startup | Activation requires network or cache is unavailable/invalid | Reconnect or show a clear retry/support path without unlocking |
Next steps
Review the Sandbox quickstart, signed responses and error handling.