Add license key activation, automatic updates, and entitlement verification to any WordPress plugin or theme. Licenzo WP SDK is a lightweight, zero-dependency PHP library that connects your WordPress product to a Licenzo license server — handling activation, validation, auto-updates, and admin UI without any external packages.
Built for WordPress developers who sell plugins and themes. Works with WooCommerce, Easy Digital Downloads, or any storefront powered by Licenzo.
- HMAC-SHA256 signed API requests
- Ed25519 signed update metadata verification
- Site-bound activation with fingerprint binding
- Native WordPress auto-updater integration
- Built-in admin license page with CSRF protection
- Freemium-to-premium upgrade flow
- Custom update channels (Stable, Beta, Nightly, or any channel you define)
- White-label admin UI with brand color support
- Custom WordPress action hooks for extensibility
- Automatic environment detection (local, staging, production)
- Zero external dependencies — pure PHP 8.0+, uses the WordPress HTTP API
- Installation
- Quick Start
- Configuration Reference
- License Activation
- License Validation
- License Status Checks
- Plan and License Information
- Package-Level Entitlements
- Admin License Page
- Automatic Updates
- Update Channels
- Freemium Mode
- White-Label Branding
- WordPress Hooks
- Security Architecture
- Server API Reference
- Environment Detection
- Background License Sync
- Grace Period
- Validation Cache
- Domain Migration Detection
- Multi-Plugin Version Safety
- Complete Plugin Example
- FAQ
- Changelog
- License
Copy the licenzo-wp-sdk directory into your project and require the loader from your main file:
require_once __DIR__ . '/licenzo-wp-sdk/start.php';The SDK registers its own PSR-4 autoloader automatically. No Composer required. When multiple plugins embed different SDK versions, the loader automatically resolves to the highest version — see Multi-Plugin Version Safety.
your-plugin/
├── licenzo-wp-sdk/
│ ├── start.php
│ ├── composer.json
│ └── src/
├── your-plugin.php
└── ...
If you use Composer in your project, add the SDK as a path repository or require it once published:
{
"require": {
"licenzo/wp-sdk": "^0.2"
}
}When installed via Composer, skip start.php — the classes are autoloaded through vendor/autoload.php.
| Requirement | Minimum Version |
|---|---|
| PHP | 8.0 |
| WordPress | 5.0 |
The SDK has zero external dependencies. It uses the WordPress HTTP API (wp_remote_request) for all server communication.
The recommended integration pattern wraps the SDK in a dedicated function. This creates a single, reusable entry point you can call from anywhere in your codebase — similar to how WooCommerce uses WC() or EDD uses EDD().
<?php
/**
* Plugin Name: Invoice Builder Pro
* Version: 1.0.0
* Requires PHP: 8.0
*/
defined( 'ABSPATH' ) || exit;
require_once __DIR__ . '/licenzo-wp-sdk/start.php';
function invoice_builder_license() {
static $client = null;
if ( $client !== null ) {
return $client;
}
$client = \Licenzo\WpSdk\Licenzo::plugin( [
'id' => 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'slug' => 'invoice-builder-pro',
'name' => 'Invoice Builder Pro',
'store' => 'https://invoicebuilder.dev',
'file' => __FILE__,
'version' => '1.0.0',
] );
$client->settings()->submenu( 'options-general.php' );
$client->updater()->enable();
$client->boot();
return $client;
}
add_action( 'plugins_loaded', 'invoice_builder_license' );// In functions.php
require_once get_template_directory() . '/licenzo-wp-sdk/start.php';
function developer_theme_license() {
static $client = null;
if ( $client !== null ) {
return $client;
}
$client = \Licenzo\WpSdk\Licenzo::theme( [
'id' => 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'slug' => 'developer-theme-pro',
'store' => 'https://developer-theme.dev',
'file' => __FILE__,
'version' => '1.0.0',
'permission' => 'edit_theme_options',
] );
$client->settings()->menu( [ 'permission' => 'edit_theme_options' ] );
$client->updater()->enable();
$client->boot();
return $client;
}
add_action( 'after_setup_theme', 'developer_theme_license' );Once your license function is defined, call it from any file in your plugin or theme:
// Gate a premium feature
if ( invoice_builder_license()->isActive() ) {
require_once __DIR__ . '/pro/pdf-export.php';
}
// Display the current plan
echo 'Plan: ' . esc_html( invoice_builder_license()->plan() );
// Check if updates are permitted
if ( invoice_builder_license()->canUpdate() ) {
// show update notification
}| Parameter | Type | Description |
|---|---|---|
id |
string |
Product UUID from the Licenzo dashboard. Used in API headers for anti-enumeration security. |
slug |
string |
Unique product slug. Used internally for WordPress option keys, cron hooks, and admin menu slugs. |
store |
string |
Your Licenzo store URL (e.g., https://store.yoursite.com). The SDK appends /wp-json/licenzo/v1 automatically. |
file |
string |
Absolute path to the main plugin or theme file. Pass __FILE__ from your root file. |
| Parameter | Type | Default | Description |
|---|---|---|---|
version |
string |
Auto-detected from plugin/theme header | Current installed version. Used as a fallback when the SDK cannot read the version from the file header. |
name |
string |
Same as slug |
Human-readable product name. Shown in admin pages and notices. |
permission |
string |
'manage_options' |
WordPress capability required for admin license pages. Use 'edit_theme_options' for themes. |
channels |
array |
[] |
Associative array of update channels (['key' => 'Label']). Shows a channel selector in the admin UI when more than one entry is provided. See Update Channels. |
hooks |
array |
[] |
Custom WordPress action hook names for lifecycle events. See WordPress Hooks. |
is_premium |
bool |
true |
Set to false for freemium mode. Disables the auto-updater and shows an upgrade CTA instead of the license activation form. |
upgrade_url |
string |
'' |
URL for the upgrade button displayed in freemium mode. |
brand_color |
string |
'' |
Hex color code (e.g., '#4f46e5') for admin UI accents. Applied to the status badge and the freemium CTA. |
verify_metadata_signature |
bool |
false |
When true, auto-update metadata from the server must be signed with Ed25519. Unsigned or tampered responses are rejected. |
metadata_public_key |
string |
'' |
Base64-encoded Ed25519 public key for verifying update metadata signatures. |
timeout |
int |
10 |
HTTP request timeout in seconds for all API calls. |
\Licenzo\WpSdk\Licenzo::plugin( [
'id' => 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'slug' => 'invoice-builder-pro',
'name' => 'Invoice Builder Pro',
'store' => 'https://invoicebuilder.dev',
'file' => __FILE__,
'version' => '2.1.0',
'permission' => 'manage_options',
'channels' => [
'stable' => 'Stable Release',
'beta' => 'Beta Release',
],
'hooks' => [
'activated' => 'invoice_builder_activated',
'deactivated' => 'invoice_builder_deactivated',
'validated' => 'invoice_builder_validated',
],
'is_premium' => true,
'upgrade_url' => 'https://invoicebuilder.dev/pricing',
'brand_color' => '#4f46e5',
'verify_metadata_signature' => true,
'metadata_public_key' => 'BASE64_ED25519_PUBLIC_KEY',
'timeout' => 15,
] );
---
## License Activation
Activate a license key for the current WordPress site. The SDK sends the key, product UUID, and a site fingerprint to the Licenzo server, receives an activation token, and persists the license state locally.
```php
$result = invoice_builder_license()->activate( 'XXXX-XXXX-XXXX-XXXX' );
if ( $result['ok'] ) {
// License activated successfully
$plan = $result['state']['meta']['plan'];
$status = $result['state']['status'];
} else {
// Activation failed
$error = $result['message'];
}On success:
[
'ok' => true,
'message' => 'License activated.',
'state' => [ /* full persisted license state */ ],
'response' => [ /* raw server response */ ],
]On failure:
[
'ok' => false,
'message' => 'License key is invalid.',
]Deactivate the license from the current site. Clears the activation token locally and notifies the server to release the activation slot.
$result = invoice_builder_license()->deactivate();
if ( $result['ok'] ) {
// License deactivated, activation slot freed
}Validate the current license against the Licenzo server. The SDK caches validation results to minimize API calls — 24 hours for active and trial licenses, 6 hours for all other states.
// Cached validation (recommended for most use cases)
$state = invoice_builder_license()->validate();
// Force a fresh server call, bypassing the cache
$state = invoice_builder_license()->validate( force: true );The returned $state array contains the full persisted license state, including status, plan, activation count, and timestamps.
Use these boolean methods to gate features, control access, or display status information in your plugin or theme.
| Method | Returns | Description |
|---|---|---|
isActive() |
bool |
true when the license status is active. |
isTrial() |
bool |
true when the license is in a trial period. |
isExpired() |
bool |
true when the license has expired. |
isGrace() |
bool |
true when the server is unreachable and the grace period is active. |
isSuspended() |
bool |
true when the license is suspended (activation limit exceeded or admin action). |
canUpdate() |
bool |
true when the license status is active, trial, or grace and the update entitlement is present. |
isPremium() |
bool |
true when running in premium mode (not freemium). Reflects the is_premium config value. |
add_action( 'plugins_loaded', function () {
if ( ! invoice_builder_license()->isActive() ) {
return;
}
require_once __DIR__ . '/pro/pdf-export.php';
require_once __DIR__ . '/pro/recurring-invoices.php';
} );if ( invoice_builder_license()->isTrial() ) {
add_action( 'admin_notices', function () {
echo '<div class="notice notice-info"><p>';
echo 'Your trial is active. <a href="https://invoicebuilder.dev/pricing">Upgrade</a> to keep access after it expires.';
echo '</p></div>';
} );
}| Method | Returns | Description |
|---|---|---|
status() |
string |
Current license status: active, inactive, expired, grace, suspended, or trial. |
plan() |
string |
Current plan name (e.g., 'Professional Yearly'). Empty string if no plan is set. |
channel() |
string |
Active update channel (e.g., 'stable', 'beta'). |
getLicense() |
array |
Full persisted license state array. |
The getLicense() method returns the complete persisted state:
[
'status' => 'active',
'message' => 'License activated.',
'license_key' => 'XXXX-XXXX-XXXX-XXXX',
'entitlements' => [],
'packages' => [],
'installation' => [
'id' => 'inst_abc123',
'secret' => '...',
],
'timestamps' => [
'activated_at' => '2026-03-19T12:00:00+00:00',
'validated_at' => '2026-03-19T14:30:00+00:00',
'last_success_at' => '2026-03-19T14:30:00+00:00',
'grace_until' => '',
],
'meta' => [
'plan' => 'Professional Yearly',
'valid_till' => '2027-03-19',
'is_lifetime' => 'no',
'activations_used' => 1,
'activations_limit' => 5,
'support_end_date' => '2027-03-19',
],
]For WordPress products that ship multiple feature packages or add-ons under a single license, pass a package key to scope the status check.
if ( invoice_builder_license()->isActive( 'addon-recurring' ) ) {
require_once __DIR__ . '/addons/recurring-invoices.php';
}
if ( invoice_builder_license()->canUpdate( 'addon-pdf' ) ) {
// PDF add-on has update access
}When no package key is passed, the check applies to the root license.
The SDK includes a production-ready WordPress admin page for license activation. It renders an activation form, status table, plan details, activation count, expiry date, and — when configured — an update channel selector. Nonce verification and capability checks are handled automatically.
// As a submenu under Settings
$client->settings()->submenu( 'options-general.php' );
// As a submenu under your own plugin menu
$client->settings()->submenu( 'invoice-builder-settings' );
// As a top-level admin menu item
$client->settings()->menu();
// With custom page and menu titles
$client->settings()->submenu( 'options-general.php', [
'page_title' => 'Invoice Builder License',
'menu_title' => 'License',
] );When the built-in admin page is enabled, a License link is automatically added to the plugin row on the WordPress Plugins page (wp-admin/plugins.php).
Disable the built-in page and use the API methods directly in your custom interface:
$client->settings()->disableBuiltInUi();
$client->boot();
// In your custom settings page:
if ( isset( $_POST['activate_license'] ) ) {
check_admin_referer( 'my_plugin_license_action' );
$result = invoice_builder_license()->activate(
sanitize_text_field( $_POST['license_key'] )
);
if ( $result['ok'] ) {
add_settings_error( 'my_plugin', 'activated', 'License activated.', 'success' );
} else {
add_settings_error( 'my_plugin', 'activation_error', esc_html( $result['message'] ), 'error' );
}
}The SDK integrates with the native WordPress update system. Your users see plugin and theme updates in Dashboard > Updates and on the Plugins page — exactly like updates from wordpress.org.
$client->updater()->enable();
$client->boot();When enabled, the SDK:
- Hooks into
pre_set_site_transient_update_plugins(or the theme equivalent) to check for new versions. - Verifies the license allows updates via the
canUpdate()check. - Sends the selected update channel to the server via the
X-Licenzo-ChannelHTTP header. - Injects the update into the WordPress transient so it appears natively.
- Handles the
plugins_apifilter for the "View Details" modal. - Caches the result for 1 hour to avoid excessive API calls.
For tamper-proof update delivery, enable Ed25519 (RFC 8032) signature verification. The Licenzo server signs the update metadata with its private key; the SDK verifies the detached signature against your embedded public key before accepting the update.
$client = \Licenzo\WpSdk\Licenzo::plugin( [
// ...
'verify_metadata_signature' => true,
'metadata_public_key' => 'BASE64_ED25519_PUBLIC_KEY',
] );When enabled, unsigned or tampered update metadata is silently rejected. The SDK uses sodium_crypto_sign_verify_detached from libsodium, which is bundled with PHP 7.2+.
Allow your users to choose which release stream to follow — Stable, Beta, Nightly, Release Candidate, or any custom channel you define.
$client = \Licenzo\WpSdk\Licenzo::plugin( [
// ...
'channels' => [
'stable' => 'Stable Release',
'beta' => 'Beta Release',
'nightly' => 'Nightly Build',
],
] );- When
channelshas more than one entry, the admin license page shows an Update Channel dropdown. - The user's selection is persisted in the WordPress options table.
- On every update check and validation request, the selected channel is sent to the Licenzo server via the
X-Licenzo-ChannelHTTP header. - The server responds with the version that matches the requested channel.
When channels is omitted or has a single entry, no dropdown is shown and the SDK uses 'stable' as the default channel.
$channel = invoice_builder_license()->channel();
// 'stable', 'beta', 'nightly', etc.Ship a free version of your WordPress plugin on wordpress.org with a built-in upgrade path to your premium version.
$client = \Licenzo\WpSdk\Licenzo::plugin( [
'id' => 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'slug' => 'invoice-builder',
'store' => 'https://invoicebuilder.dev',
'file' => __FILE__,
'is_premium' => false,
'upgrade_url' => 'https://invoicebuilder.dev/pricing',
'brand_color' => '#4f46e5',
] );
$client->settings()->submenu( 'options-general.php' );
$client->boot();In freemium mode:
- The auto-updater is disabled — the free version receives updates from wordpress.org.
- The admin license page shows a branded Upgrade to Pro CTA with a link to your
upgrade_url. isPremium()returnsfalse, allowing you to gate features accordingly.
if ( invoice_builder_license()->isPremium() && invoice_builder_license()->isActive() ) {
// Premium features
} else {
// Free tier or inactive
}Customize the admin UI to match your product's visual identity with a single hex color value.
'brand_color' => '#4f46e5',The brand_color is applied to:
- The status text when the license is active or in trial.
- The freemium CTA background and button accent color.
The SDK fires WordPress actions at three lifecycle moments: activation, deactivation, and validation. You can use the default hook names or provide your own to avoid conflicts when multiple plugins embed the SDK.
When no hooks config is provided, the SDK fires these actions:
| Lifecycle Event | Default Action Hook | Parameters |
|---|---|---|
| License activated | licenzo_sdk_activated |
string $slug, array $state |
| License deactivated | licenzo_sdk_deactivated |
string $slug, array $state |
| License validated | licenzo_sdk_validated |
string $slug, array $state |
add_action( 'licenzo_sdk_activated', function ( string $slug, array $state ) {
update_option( 'my_plugin_activated_at', gmdate( 'c' ) );
}, 10, 2 );When multiple plugins embed the SDK on the same WordPress site, the default licenzo_sdk_* hooks fire for every plugin. To namespace your hooks and avoid cross-plugin conflicts, provide a hooks config:
$client = \Licenzo\WpSdk\Licenzo::plugin( [
// ...
'hooks' => [
'activated' => 'invoice_builder_activated',
'deactivated' => 'invoice_builder_deactivated',
'validated' => 'invoice_builder_validated',
],
] );Now the SDK fires invoice_builder_activated instead of licenzo_sdk_activated:
add_action( 'invoice_builder_activated', function ( string $slug, array $state ) {
update_option( 'invoice_builder_onboarding', 'yes' );
}, 10, 2 );
add_action( 'invoice_builder_validated', function ( string $slug, array $state ) {
if ( $state['status'] === 'expired' ) {
delete_transient( 'invoice_builder_premium_cache' );
}
}, 10, 2 );You can override one, two, or all three hooks. Any hook not specified in the config falls back to the licenzo_sdk_* default.
$hook_name = invoice_builder_license()->hook( 'activated' );
// Returns 'invoice_builder_activated' or 'licenzo_sdk_activated' (default)| Filter | Parameters | Description |
|---|---|---|
licenzo_should_count_activation |
bool $should_count |
Controls whether the current site counts toward the activation limit. Returns false for local environments by default. |
add_filter( 'licenzo_should_count_activation', function ( bool $should_count ) {
if ( wp_get_environment_type() === 'staging' ) {
return false;
}
return $should_count;
} );The SDK implements multiple cryptographic layers to protect your licensing infrastructure against replay attacks, payload tampering, and unauthorized distribution.
Every API request after initial activation is signed using HMAC-SHA256 with a per-installation secret. The canonical signing string covers five components to prevent replay and tampering:
CANONICAL = HTTP_METHOD + "\n"
+ REQUEST_PATH + "\n"
+ SHA256( request_body ) + "\n"
+ UNIX_TIMESTAMP + "\n"
+ CRYPTOGRAPHIC_NONCE
SIGNATURE = Base64( HMAC-SHA256( CANONICAL, installation_secret ) )
The signature is transmitted in the X-Licenzo-Signature header as v1=<base64_signature>. The nonce is generated using random_bytes() (CSPRNG).
Update metadata from the Licenzo server can be cryptographically signed using Ed25519 (RFC 8032). The SDK verifies the detached signature using sodium_crypto_sign_verify_detached from libsodium. When verify_metadata_signature is enabled, unsigned or tampered metadata is rejected before any update is applied.
Every activation is cryptographically bound to the site's home_url via an MD5 fingerprint. The fingerprint is included in every API request. If a request arrives from a different fingerprint than the one the activation token was issued to, the server rejects it immediately — no grace period.
After a successful activation, the server issues an opaque activation token. All subsequent requests (validation, update checks) use this token instead of the raw license key, minimizing key exposure over the network.
Downloaded update packages can be verified against a SHA-256 hash provided by the server, ensuring file integrity before installation.
Failed API requests (connection errors and HTTP 5xx responses) are retried once after a 500ms delay before being treated as a failure.
The Licenzo store exposes a public REST API under the WordPress REST namespace. The SDK communicates with these endpoints automatically — this section documents them for debugging, custom integrations, or building SDKs in other languages.
All endpoints live under:
{store_url}/wp-json/licenzo/v1
For example, if your store URL is https://invoicebuilder.dev:
POST https://invoicebuilder.dev/wp-json/licenzo/v1/activate
The SDK constructs this base URL automatically from the store config value.
The store API uses custom HTTP headers for authentication. Which headers are required depends on the endpoint.
| Header | Description |
|---|---|
X-Licenzo-License-Key |
Raw license key string. Required for activation only. |
X-Licenzo-Activation-Token |
Opaque HMAC token issued on activation. Required for validate, deactivate, and update endpoints. |
X-Licenzo-Fingerprint |
Site fingerprint — md5( home_url('/') ). Required on all endpoints. Binds the activation to a specific site. |
X-Licenzo-Product |
Product UUID from the Licenzo dashboard. Required for activation. |
X-Licenzo-Channel |
Update channel (stable, beta, rc, alpha). Optional — defaults to stable. Sent on validate and update endpoints. |
All endpoints return errors in this shape:
{
"success": false,
"message": "Human-readable error description",
"status": 403
}When WP_DEBUG is enabled on the store, an additional error field contains the exception message. The code field appears on specific errors (e.g., fingerprint_mismatch).
Activate a license key for a site. Issues an activation token that all subsequent requests use.
Required Headers:
| Header | Value |
|---|---|
X-Licenzo-License-Key |
The license key to activate |
X-Licenzo-Fingerprint |
md5( home_url('/') ) |
X-Licenzo-Product |
Product UUID |
Body Parameters:
| Field | Type | Description |
|---|---|---|
instance_identifier |
string |
Unique site identifier. Defaults to the fingerprint when not provided. |
site_url |
string |
The site's site_url(). |
home_url |
string |
The site's home_url(). |
Success Response:
{
"success": true,
"message": "License activated",
"data": {
"activation_token": "eyJh...signature",
"activation_id": 42,
"license_key": "XXXX-XXXX-XXXX-XXXX",
"license": {
"status": "active",
"plan": "Professional Yearly",
"expires_at": "2027-03-19",
"is_lifetime": false
},
"activations": {
"used": 1,
"limit": 5
},
"support_end_date": "2027-03-19"
}
}| Field | Notes |
|---|---|
activation_token |
HMAC-signed token. Store it — all subsequent requests require it. |
license.plan |
Variation name (e.g., "Professional Yearly"). Empty string if no variation. |
license.expires_at |
null for lifetime licenses. Date string YYYY-MM-DD otherwise. |
license.is_lifetime |
true when the variation billing cycle is lifetime or onetime. |
support_end_date |
null when no support expiry is set. |
Validate an existing activation. Confirms the license is still active and the activation token is valid for this site.
Required Headers:
| Header | Value |
|---|---|
X-Licenzo-Activation-Token |
Token from /activate |
X-Licenzo-Fingerprint |
md5( home_url('/') ) |
Optional Headers:
| Header | Value |
|---|---|
X-Licenzo-Channel |
Active update channel |
Body Parameters: None required. The SDK sends environment context but the server does not require specific body fields.
Success Response:
Same data shape as /activate:
{
"success": true,
"message": "License is valid",
"data": {
"activation_token": "eyJh...signature",
"activation_id": 42,
"license_key": "XXXX-XXXX-XXXX-XXXX",
"license": {
"status": "active",
"plan": "Professional Yearly",
"expires_at": "2027-03-19",
"is_lifetime": false
},
"activations": {
"used": 1,
"limit": 5
},
"support_end_date": "2027-03-19"
}
}Error Codes:
| Code | Meaning |
|---|---|
fingerprint_mismatch |
The fingerprint does not match the one bound to the activation token. The request is rejected — the client should clear local state and re-activate. |
Deactivate an activation. Releases the activation slot so it can be used on another site.
Required Headers:
| Header | Value |
|---|---|
X-Licenzo-Activation-Token |
Token from /activate |
X-Licenzo-Fingerprint |
md5( home_url('/') ) |
Body Parameters: None required.
Success Response:
{
"success": true,
"message": "License deactivated"
}The data field is omitted on success.
Check whether a newer version of the product is available. The server validates the activation token, resolves the product and variation from the activation record, and returns the latest release metadata.
Required Headers:
| Header | Value |
|---|---|
X-Licenzo-Activation-Token |
Token from /activate |
X-Licenzo-Fingerprint |
md5( home_url('/') ) |
Optional Headers:
| Header | Value |
|---|---|
X-Licenzo-Channel |
stable (default), beta, rc, or alpha. Takes precedence over the body channel field. |
Body Parameters:
| Field | Type | Description |
|---|---|---|
current_version |
string |
Currently installed version. Used to determine if an update is available. The server also accepts installed_version or version as alternatives. |
channel |
string |
Update channel. Overridden by the X-Licenzo-Channel header when present. |
site_url |
string |
The site's site_url(). |
home_url |
string |
The site's home_url(). |
plugin |
string |
Plugin basename (e.g., invoice-builder-pro/invoice-builder-pro.php). |
slug |
string |
Product slug. |
Success Response (update available):
{
"success": true,
"data": {
"license": {
"id": 1,
"status": "active",
"expire_date": "2027-03-19",
"support_till": "2027-03-19"
},
"product": {
"id": 1,
"uuid": "a1b2c3d4-...",
"name": "Invoice Builder Pro",
"variation": {
"id": 1,
"uuid": "f1e2d3c4-...",
"name": "Professional Yearly"
}
},
"channel": "stable",
"current_version": "2.1.0",
"latest_version": "2.2.0",
"update_available": true,
"release": {
"uuid": "rel-uuid-...",
"title": "Version 2.2.0",
"version": "2.2.0",
"status": "published",
"channel": "stable",
"is_available": true,
"release_date": "2026-03-15",
"changelog": "<ul><li>New feature</li></ul>",
"sdk_meta": {},
"variation_package": {
"variation_id": 1,
"variation_uuid": "f1e2d3c4-...",
"variation_name": "Professional Yearly",
"media": {},
"woo_asset": {}
},
"updated_at": "2026-03-15 10:30:00"
},
"sdk": {
"slug": "invoice-builder-pro",
"plugin": "invoice-builder-pro/invoice-builder-pro.php",
"version": "2.2.0",
"package": "https://store.example.com/wp-json/licenzo/v1/updates/download/a1b2c3d4e5f6...?release=rel-uuid-...&lid=1&vid=1",
"tested": "6.7",
"requires": "5.0",
"requires_php": "8.0",
"last_updated": "2026-03-15 00:00:00",
"homepage": "https://invoicebuilder.dev",
"url": "https://invoicebuilder.dev",
"upgrade_notice": "",
"sections": {
"changelog": "<ul><li>New feature</li></ul>"
},
"banners": {},
"icons": {},
"compatibility": {},
"extra": {},
"variation": {
"id": 1,
"uuid": "f1e2d3c4-...",
"name": "Professional Yearly"
}
}
}
}| Field | Notes |
|---|---|
data.update_available |
true when latest_version is greater than current_version. |
data.sdk |
WordPress-compatible update payload. The SDK maps this directly into the update_plugins transient and the plugins_api response. |
data.sdk.package |
Signed download URL with an opaque token in the path and release/variation context in query params. No expiry — access is gated by license status and entitlement at download time. |
data.release |
Full release metadata including changelog, SDK meta, and variation package info. |
data.license.expire_date |
null for lifetime licenses. |
Success Response (no update):
{
"success": true,
"data": {
"license": { "..." },
"product": { "..." },
"channel": "stable",
"current_version": "2.2.0",
"update_available": false,
"message": "No published release available for this variation."
}
}Retrieve detailed update metadata for the "View Details" modal in WordPress admin. Uses the same authentication and body parameters as /updates/check.
When no update is available, this endpoint still returns the latest release metadata (unlike /updates/check), making it suitable for the plugin information dialog.
Headers: Same as /updates/check.
Body Parameters: Same as /updates/check.
Success Response: Same shape as /updates/check.
Download the update package. The {token} is an opaque, server-generated credential embedded in the sdk.package URL from the update check response. The SDK never constructs this URL — it is passed directly to WordPress's native updater.
| Parameter | Location | Description |
|---|---|---|
token |
URL path | Opaque server-generated token that identifies and authenticates the requesting license |
release |
Query param | Release UUID — identifies which release to download |
lid |
Query param | License ID — used for O(1) license lookup |
vid |
Query param | Variation ID — identifies which variation package to serve |
No authentication headers required. Authentication is embedded in the URL token and verified server-side.
Success Response (302): Redirects to the package file URL with Location header and Cache-Control: no-store, no-cache, must-revalidate, max-age=0.
{
"success": true,
"message": "Redirecting to release package.",
"release_uuid": "rel-uuid-...",
"version": "2.2.0"
}Error Responses:
| Status | Condition |
|---|---|
| 400 | Missing required download parameters (token, release, lid, or vid) |
| 401 | Token is invalid for the given license |
| 403 | License is not active, has expired, is not entitled to the variation, or the release is not published |
| 404 | License, variation, product, release, or package not found |
| 500 | Package file URL could not be resolved |
The SDK automatically classifies the current WordPress installation to determine activation counting behavior.
| Environment | Detection Criteria |
|---|---|
| Local | WP_ENVIRONMENT_TYPE set to local or development; TLD is .local, .test, .dev, or .localhost; host is localhost; or IP is in a private range (127.x, 10.x, 192.168.x, 172.16-31.x). |
| Staging | WP_ENVIRONMENT_TYPE set to staging; hostname contains staging, stage, or dev; or domain matches a known hosting staging suffix (.wpengine.com, .kinsta.cloud, .pantheonsite.io, .flywheelstaging.com). |
| Production | All other environments. |
Local environments are excluded from activation counts by default. This means developers can test locally without consuming an activation slot. Override this behavior with the licenzo_should_count_activation filter.
The SDK schedules a daily WP-Cron event that re-validates the license in the background, keeping the local state current even when no admin visits the dashboard.
- Cron hook name:
licenzo_background_sync_{slug} - Frequency: Once daily
- Cleanup: The cron event is automatically unscheduled when the plugin is deactivated via
register_deactivation_hook.
If the Licenzo server is temporarily unreachable during validation, the SDK enters a grace period instead of revoking access immediately. This prevents false-positive lockouts caused by transient network issues, DNS failures, or server maintenance windows.
| Scenario | Duration | Behavior |
|---|---|---|
| Server unreachable, last successful validation within 7 days | Remaining time until 7 days after last success | isGrace() returns true, canUpdate() returns true, isActive() returns false |
| Server unreachable, last successful validation more than 7 days ago | Immediate | Status reverts to last known state |
The SDK caches validation responses locally to minimize API calls to the Licenzo server.
| License Status | Cache Duration |
|---|---|
active or trial |
24 hours |
| All other statuses | 6 hours |
To force a fresh server call and bypass the cache:
$state = invoice_builder_license()->validate( force: true );When a WordPress site's home_url changes — common during staging-to-production migration, domain changes, or site cloning — the SDK detects the mismatch and displays a WordPress admin notice:
Your site URL has changed. Please re-activate your license.
The previous activation token is bound to the original site fingerprint and will be rejected by the server. Re-activating claims a new activation slot.
When multiple plugins on the same WordPress site embed different SDK versions, the loader uses a "highest version wins" registry pattern to prevent class conflicts and ensure the newest code runs.
- Each plugin's
require_once 'start.php'registers its version and directory with a globalLicenzo_SDK_Registry - No autoloader is registered during file load — only candidates are collected
- At
plugins_loadedpriority0(before user code at priority10), the registry sorts all candidates by version, selects the highest, and registers a single PSR-4 autoloader for that version - A lazy autoloader provides a safety net for early class access before
plugins_loadedfires
Timeline:
Plugin A loads → registers 0.2.0
Plugin B loads → registers 0.3.0
plugins_loaded:0 → Registry resolves → 0.3.0 autoloader registered ✓
plugins_loaded:10 → User code → Licenzo 0.3.0 classes available ✓
For version-safe loading, always initialize the SDK inside a hook callback — not at the top level of your plugin file:
// ✓ Correct — SDK initialized inside a hook
require_once __DIR__ . '/licenzo-wp-sdk/start.php';
function my_plugin_license() {
static $client = null;
if ($client !== null) return $client;
$client = \Licenzo\WpSdk\Licenzo::plugin([/* config */]);
return $client;
}
add_action('plugins_loaded', 'my_plugin_license');// ✗ Avoid — direct instantiation during file load
require_once __DIR__ . '/licenzo-wp-sdk/start.php';
$client = \Licenzo\WpSdk\Licenzo::plugin([/* config */]); // May resolve wrong versionThe SDK follows semantic versioning. Minor and patch releases maintain backward compatibility. When the registry selects a newer version, all plugins benefit from bug fixes and improvements without any code changes.
A production-ready integration demonstrating initialization, feature gating, custom hooks, and event handling:
<?php
/**
* Plugin Name: Invoice Builder Pro
* Description: Professional invoicing for WordPress.
* Version: 2.1.0
* Requires PHP: 8.0
*/
defined( 'ABSPATH' ) || exit;
require_once __DIR__ . '/licenzo-wp-sdk/start.php';
function invoice_builder_license() {
static $client = null;
if ( $client !== null ) {
return $client;
}
$client = \Licenzo\WpSdk\Licenzo::plugin( [
'id' => 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'slug' => 'invoice-builder-pro',
'name' => 'Invoice Builder Pro',
'store' => 'https://invoicebuilder.dev',
'file' => __FILE__,
'version' => '2.1.0',
'brand_color' => '#0ea5e9',
'channels' => [
'stable' => 'Stable',
'beta' => 'Beta',
],
'hooks' => [
'activated' => 'invoice_builder_activated',
'deactivated' => 'invoice_builder_deactivated',
'validated' => 'invoice_builder_validated',
],
] );
$client->settings()->submenu( 'invoice-builder-settings' );
$client->updater()->enable();
$client->boot();
return $client;
}
add_action( 'plugins_loaded', 'invoice_builder_license' );
// ── Feature Gating ──────────────────────────────────────
add_action( 'plugins_loaded', function () {
if ( ! invoice_builder_license()->isActive() ) {
return;
}
require_once __DIR__ . '/pro/pdf-export.php';
require_once __DIR__ . '/pro/recurring-invoices.php';
require_once __DIR__ . '/pro/tax-reports.php';
}, 20 );
// ── Lifecycle Events ────────────────────────────────────
add_action( 'invoice_builder_activated', function ( string $slug, array $state ) {
update_option( 'invoice_builder_onboarding', 'yes' );
}, 10, 2 );
add_action( 'invoice_builder_validated', function ( string $slug, array $state ) {
if ( $state['status'] === 'expired' ) {
delete_transient( 'invoice_builder_premium_cache' );
}
}, 10, 2 );Install the Licenzo WP SDK by copying the licenzo-wp-sdk directory into your plugin. Create a license function (see Quick Start) and call $client->settings()->submenu('options-general.php') to add a license activation page under Settings. The SDK handles the activation form, nonce verification, and server communication.
Enable the SDK's built-in updater with $client->updater()->enable(). The SDK hooks into the native WordPress update system and checks your Licenzo server for new versions. Updates appear in Dashboard > Updates and on the Plugins page, just like wordpress.org updates.
The SDK enters a 7-day grace period. Your users' plugins and themes continue to function normally. After 7 days without a successful validation, the license status reverts to its last known state. See Grace Period.
Yes. The SDK detects multisite installations and includes this information in API payloads. Each sub-site on the network requires its own license activation.
Local development environments — .local, .test, .dev, .localhost domains and private IP addresses — are excluded from activation counts automatically. No configuration needed. See Environment Detection.
Yes. Use 'is_premium' => false in the free version and 'is_premium' => true in the pro version. Both can embed the same SDK. The free version shows an upgrade CTA while the pro version shows the activation form. See Freemium Mode.
| Operation | Frequency |
|---|---|
| License validation | Cached for 24 hours (active/trial) or 6 hours (other statuses). |
| Update check | Cached for 1 hour. |
| Background sync | Once daily via WP-Cron. |
Yes. Call $client->settings()->disableBuiltInUi() and use the API methods — activate(), deactivate(), validate(), status(), plan() — directly in your custom settings page.
Every API request includes site URL, domain, locale, WordPress version, PHP version, product slug, UUID, version, SDK version, and environment classification (local/staging/production). Authentication uses either the license key (on first activation) or an activation token (for all subsequent requests). No admin emails, server IP addresses, or personal user data is transmitted.
Pass a hooks config to namespace your lifecycle hooks. For example, 'hooks' => ['activated' => 'my_plugin_activated'] fires my_plugin_activated instead of the default licenzo_sdk_activated. See WordPress Hooks.
The global Licenzo_SDK_Registry collects all embedded versions and resolves to the highest at plugins_loaded priority 0. Only one autoloader is registered. All plugins share the winning version's classes. This requires the singleton-function pattern inside a hook — direct top-level instantiation may resolve the wrong version. See Multi-Plugin Version Safety.
HMAC-SHA256 for request signing, SHA-256 for body hashing, Ed25519 (RFC 8032) for update metadata verification, and MD5 for site fingerprinting. Nonces are generated using random_bytes() (CSPRNG). See Security Architecture.
- Static entry point:
Licenzo::plugin()/Licenzo::theme() - Flat public API:
activate(),deactivate(),validate(),isActive(),plan(),status(),getLicense(),canUpdate() - Simplified configuration:
id,slug,store,fileminimum - Server-side variation resolution — no
variation_uuidrequired in SDK config - Custom update channels via
channelsconfig with admin UI selector - Channel transport via
X-Licenzo-ChannelHTTP header - Custom WordPress action hook names via
hooksconfig - Built-in admin license page: status, plan, activation count, expiry, channel selector
- Plugin action link on WordPress Plugins page
- White-label admin UI via
brand_color - Freemium mode with branded upgrade CTA
- HMAC-SHA256 request signing with SHA-256 body hash, timestamp, and cryptographic nonce
- Ed25519 update metadata signature verification
- Site fingerprint binding with MD5 hash of
home_url - Activation token model — license key is not exposed after initial activation
- 7-day grace period on server unreachability
- Validation caching: 24 hours (active/trial), 6 hours (other)
- Background license sync via daily WP-Cron
- Domain migration detection with admin notice
- Automatic environment detection (local/staging/production)
- Local environments excluded from activation counts
- Expiry warning notices: 30-day and 7-day thresholds, dismissible
- Plugin deactivation cleanup: background sync cron automatically unscheduled
- HTTP retry: single retry with 500ms backoff on connection errors and 5xx responses
- Boolean state values stored as
yes/nostrings for clean database representation - Version-safe multi-plugin loader: global registry resolves to the highest embedded SDK version