code-heaven / license-sdk
Vendor SDK for the Code Heaven Developer License API (validation, domain activation, updates, downloads).
Requires
- php: >=8.0
Requires (Dev)
- phpunit/phpunit: ^10
README
Vendor SDK for the Code Heaven Developer License API. Use it inside your plugin/theme/app to validate licenses, activate and free domain seats, check for updates, and fetch signed download URLs.
This is the vendor server-to-server flow. Authentication is a single vendor
API key (X-CH-Vendor-Key), not the buyer OAuth flow.
- Base URL:
https://api.code-heaven.com/v1 - PHP 8.0+
- No hard runtime dependencies (cURL or
file_get_contents); pluggable HTTP so it works happily inside WordPress viawp_remote_request.
Install
composer require code-heaven/license-sdk
Quick start: validate and gate
use CodeHeaven\License\Client; use CodeHeaven\License\LicenseException; use CodeHeaven\License\TransportException; $client = new Client('YOUR_VENDOR_KEY', [ 'cacheTtl' => 12 * 3600, // serve a fresh answer for 12h 'offlineGrace' => 14 * 24 * 3600, // keep working up to 14 days if the API is down ]); try { $res = $client->validate('LICENSE-KEY', 'shop.example.com', 'booknetic-pro'); } catch (LicenseException $e) { // 403: invalid / expired / domain_not_activated ($e->apiCode tells you which) bail("License problem: {$e->apiCode}"); } catch (TransportException $e) { // API unreachable AND no cached valid result within grace. bail('License server unreachable.'); } if ($res['valid']) { enable_premium_features(); } else { // $res['status'] is one of: valid | invalid | expired | domain_not_activated | revoked disable_premium_features($res['status']); }
validate() returns the decoded API body plus two SDK markers:
| key | meaning |
|---|---|
valid |
bool |
status |
valid | invalid | expired | domain_not_activated | revoked |
product |
product slug (or null) |
expiresAt |
ISO 8601 string or null |
activations |
[{domain, activatedAt}, ...] |
seatLimit |
int |
_cached |
present & true when served from cache |
_offline |
present & true when served from cache during an outage |
Activate a seat on first run
When validate() returns domain_not_activated (a LicenseException with
apiCode === 'domain_not_activated'), claim a seat:
use CodeHeaven\License\SeatLimitException; try { $client->activateDomain('LICENSE-KEY', 'shop.example.com'); } catch (SeatLimitException $e) { // 409 — buyer has used every seat; prompt them to deactivate another site. }
Check for and apply an update
$update = $client->checkUpdate('LICENSE-KEY', 'booknetic-pro', '4.1.0', 'shop.example.com'); if ($update['hasUpdate']) { // latestVersion + changelog[{version,date,notes}] $dl = $client->download('LICENSE-KEY', 'booknetic-pro', 'shop.example.com', $update['latestVersion']); // $dl['url'] is a short-lived signed package URL; $dl['expiresAt'] tells you when it dies. download_and_install_zip($dl['url']); }
Free the seat on uninstall
$client->deactivateDomain('LICENSE-KEY', 'shop.example.com');
Caching and offline grace
validate() is cached so repeated calls in one request never hit the wire
twice, and the result persists across requests when you supply a persistent
cache:
cacheTtl— how long a cached answer is considered fresh. Within this windowvalidate()returns the cached body without any network call.offlineGrace— how long a cachedvalidanswer may be served after it goes stale, but only when the API is unreachable. This stops a transient outage from locking out paying customers. Served results carry_offline => true.
Persistent caches:
use CodeHeaven\License\FileCache; use CodeHeaven\License\TransientCache; // Outside WordPress: new Client($key, ['cache' => new FileCache('/var/cache/myplugin')]); // Inside WordPress (rides transients / the object cache): new Client($key, ['cache' => new TransientCache('myplugin_')]);
The default is an in-process ArrayCache (no cross-request persistence, so no
offline grace across requests — supply FileCache/TransientCache in
production).
Custom HTTP transport (WordPress, Guzzle, …)
Inject any callable as http. It receives (method, url, headers, ?body) and
must return ['status' => int, 'headers' => array, 'body' => string], or throw
TransportException on a connection failure (which triggers offline grace).
new Client($key, [ 'http' => function (string $method, string $url, array $headers, ?string $body): array { $res = wp_remote_request($url, compact('method', 'headers', 'body') + ['timeout' => 15]); if (is_wp_error($res)) { throw new \CodeHeaven\License\TransportException($res->get_error_message()); } return [ 'status' => (int) wp_remote_retrieve_response_code($res), 'headers' => wp_remote_retrieve_headers($res)->getAll(), 'body' => (string) wp_remote_retrieve_body($res), ]; }, ]);
See examples/gate-plugin.php for a complete,
fail-safe WordPress gate (validate → activate-on-first-run → admin nag →
deactivate-on-uninstall).
Exceptions
All extend CodeHeaven\License\CodeHeavenException and expose
->statusCode, ->apiCode, ->response.
| Exception | HTTP | When |
|---|---|---|
AuthException |
401 | Vendor key missing/invalid (your packaging bug) |
LicenseException |
403 | license_invalid / expired / domain_not_activated |
SeatLimitException |
409 | seat_limit_exceeded on activateDomain() |
RateLimitException |
429 | Rate limited (->retryAfter holds the hint, if any) |
TransportException |
— | API unreachable and no usable cached result |
CodeHeavenException |
4xx/5xx | Any other non-2xx response |
Note: an invalid or expired license that the API reports with HTTP 200 and
valid:falseis returned as data, not thrown. The API only throwsLicenseExceptionfor the 403 cases above.
Development
composer install composer test # phpunit
License
MIT.