Skip to content

licenzo/php-sdk

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Licenzo WP SDK — WordPress Licensing SDK for Plugins and Themes

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

Table of Contents


Installation

Embed in Your Plugin or Theme

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
└── ...

Composer

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.

System Requirements

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.


Quick Start

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().

WordPress Plugin

<?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' );

WordPress Theme

// 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' );

Use It Anywhere in Your Code

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
}

Configuration Reference

Required Parameters

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.

Optional Parameters

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.

Full Configuration Example

\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'];
}

Activation Response

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.',
]

Deactivation

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
}

License Validation

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.


License Status Checks

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.

Feature Gating Example

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';
} );

Trial Awareness Example

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>';
    } );
}

Plan and License Information

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.

License State Structure

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',
    ],
]

Package-Level Entitlements

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.


Admin License Page

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.

Register the Page

// 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',
] );

Plugin Action Link

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).

Build Your Own Activation UI

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' );
    }
}

Automatic Updates

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.

Enable Auto-Updates

$client->updater()->enable();
$client->boot();

When enabled, the SDK:

  1. Hooks into pre_set_site_transient_update_plugins (or the theme equivalent) to check for new versions.
  2. Verifies the license allows updates via the canUpdate() check.
  3. Sends the selected update channel to the server via the X-Licenzo-Channel HTTP header.
  4. Injects the update into the WordPress transient so it appears natively.
  5. Handles the plugins_api filter for the "View Details" modal.
  6. Caches the result for 1 hour to avoid excessive API calls.

Ed25519 Update Metadata Signing

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+.


Update Channels

Allow your users to choose which release stream to follow — Stable, Beta, Nightly, Release Candidate, or any custom channel you define.

Configuration

$client = \Licenzo\WpSdk\Licenzo::plugin( [
    // ...
    'channels' => [
        'stable'  => 'Stable Release',
        'beta'    => 'Beta Release',
        'nightly' => 'Nightly Build',
    ],
] );

Behavior

  1. When channels has more than one entry, the admin license page shows an Update Channel dropdown.
  2. The user's selection is persisted in the WordPress options table.
  3. On every update check and validation request, the selected channel is sent to the Licenzo server via the X-Licenzo-Channel HTTP header.
  4. 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.

Read the Active Channel

$channel = invoice_builder_license()->channel();
// 'stable', 'beta', 'nightly', etc.

Freemium Mode

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() returns false, allowing you to gate features accordingly.
if ( invoice_builder_license()->isPremium() && invoice_builder_license()->isActive() ) {
    // Premium features
} else {
    // Free tier or inactive
}

White-Label Branding

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.

WordPress Hooks

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.

Default Hook Names

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 );

Custom Hook Names

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.

Resolve a Hook Name Programmatically

$hook_name = invoice_builder_license()->hook( 'activated' );
// Returns 'invoice_builder_activated' or 'licenzo_sdk_activated' (default)

Filters

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;
} );

Security Architecture

The SDK implements multiple cryptographic layers to protect your licensing infrastructure against replay attacks, payload tampering, and unauthorized distribution.

HMAC-SHA256 Request Signing

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).

Ed25519 Metadata Verification

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.

Site Fingerprint Binding

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.

Activation Token Model

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.

Package Integrity Verification

Downloaded update packages can be verified against a SHA-256 hash provided by the server, ensuring file integrity before installation.

Retry with Backoff

Failed API requests (connection errors and HTTP 5xx responses) are retried once after a 500ms delay before being treated as a failure.


Server API Reference

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.

Base URL

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.

Authentication Headers

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.

Error Response Format

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).


POST /activate

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.

POST /validate

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.

POST /deactivate

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.


POST /updates/check

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."
  }
}

POST /updates/info

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.


GET /updates/download/{token}

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

Environment Detection

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.


Background License Sync

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.

Grace Period

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

Validation Cache

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 );

Domain Migration Detection

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.


Multi-Plugin Version Safety

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.

How It Works

  1. Each plugin's require_once 'start.php' registers its version and directory with a global Licenzo_SDK_Registry
  2. No autoloader is registered during file load — only candidates are collected
  3. At plugins_loaded priority 0 (before user code at priority 10), the registry sorts all candidates by version, selects the highest, and registers a single PSR-4 autoloader for that version
  4. A lazy autoloader provides a safety net for early class access before plugins_loaded fires
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 ✓

Requirements

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 version

Semver Contract

The 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.


Complete Plugin Example

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 );

FAQ

How do I add a license key to my WordPress plugin?

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.

How do I add automatic updates to a WordPress plugin from a custom server?

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.

What happens if my license server is down?

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.

Does the Licenzo WP SDK work with WordPress multisite?

Yes. The SDK detects multisite installations and includes this information in API payloads. Each sub-site on the network requires its own license activation.

How do I test licensing locally without using an activation slot?

Local development environments — .local, .test, .dev, .localhost domains and private IP addresses — are excluded from activation counts automatically. No configuration needed. See Environment Detection.

Can I ship a free and a pro version of my WordPress plugin?

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.

How often does the SDK contact the license server?

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.

Can I build a custom license activation page instead of using the built-in one?

Yes. Call $client->settings()->disableBuiltInUi() and use the API methods — activate(), deactivate(), validate(), status(), plan() — directly in your custom settings page.

What data does the SDK send to the license server?

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.

How do I avoid hook name conflicts when multiple plugins use the SDK?

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.

What happens when two plugins embed different SDK versions?

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.

What cryptographic standards does the SDK use?

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.


Changelog

0.2.0

  • Static entry point: Licenzo::plugin() / Licenzo::theme()
  • Flat public API: activate(), deactivate(), validate(), isActive(), plan(), status(), getLicense(), canUpdate()
  • Simplified configuration: id, slug, store, file minimum
  • Server-side variation resolution — no variation_uuid required in SDK config
  • Custom update channels via channels config with admin UI selector
  • Channel transport via X-Licenzo-Channel HTTP header
  • Custom WordPress action hook names via hooks config
  • 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/no strings for clean database representation
  • Version-safe multi-plugin loader: global registry resolves to the highest embedded SDK version

License

MIT