All Guides
UnifiedLicensing · Integration Guide

Licensing Your WordPress Plugin

Ship your plugin like a pro: validate licenses on activation, gate premium features, add an AJAX activation form, and even wire up WooCommerce — all with one PHP SDK file.

01Quick Start

Howdy! Let's get your plugin licensed in about five minutes. The whole thing boils down to three moves:

  1. Copy the SDK into your plugin's includes/ folder.
  2. Require it from your main plugin file.
  3. Validate the license when the plugin activates.

Step 1 — Copy the SDK

Grab UnifiedLicensing.PHP.v4.0-Complete.php from your UnifiedLicensing dashboard downloads and drop it into your plugin:

bash# From your plugin's root directory
mkdir -p includes
cp ~/Downloads/UnifiedLicensing.PHP.v4.0-Complete.php includes/

Step 2 — Require it

In your main plugin file, load the SDK before anything else uses it:

php<?php
/**
 * Plugin Name: My Awesome Plugin
 * Version:     1.0.0
 */

require_once plugin_dir_path( __FILE__ ) . 'includes/UnifiedLicensing.PHP.v4.0-Complete.php';

Step 3 — Validate on activation

The SDK gives you a single call that talks to the API for you:

php$result = UnifiedLicensing::validateLicense([
    'api_key'     => 'ul_live_xxxxxxxxxxxx',
    'product_key' => 'prod_my-awesome-plugin',
    'license_key' => 'XXXX-XXXX-XXXX-XXXX',
    'machine_id'  => wp_parse_url( home_url(), PHP_URL_HOST ),
    'platform'    => 'php',
]);

// Why 'php'? The SDK auto-detects platform, but WordPress admin
// runs in a browser — so it would detect as 'web' (client-side).
// Explicitly setting 'php' correctly identifies this as a
// server-side app and matches the "Server-Side Web Applications"
// category in your dashboard.

if ( ! empty( $result['valid'] ) ) {
    // You're golden. Store the license key so we remember it.
}
That's genuinely it. Everything below builds on these three steps — settings pages, AJAX forms, WooCommerce, trials, and a full reference plugin you can copy wholesale.

02Plugin Structure

Here's the folder layout we'll build toward. The only hard requirement is that the SDK lives somewhere your plugin can require it — includes/ is the WordPress convention:

textmy-plugin/
├── my-plugin.php                                  ← Main plugin file (header + bootstrap)
├── includes/
│   └── UnifiedLicensing.PHP.v4.0-Complete.php     ← The licensing SDK
├── admin/
│   ├── class-settings-page.php                    ← Settings screen (optional)
│   └── js/
│       └── license-activation.js                  ← AJAX form script (optional)
└── readme.txt

A few friendly notes on the layout:

03Activation Hook

When someone clicks Activate, WordPress fires register_activation_hook. That's our first chance to check for a stored license key and validate it against the API.

php<?php
/**
 * Runs once, when the plugin is activated.
 */
function myplugin_activate() {

    // Grab any license key saved previously (e.g. by our settings page).
    $license_key = get_option( 'myplugin_license_key', '' );

    if ( empty( $license_key ) ) {
        // No key yet — not an error. We'll nag politely via admin notice.
        update_option( 'myplugin_license_status', 'missing' );
        return;
    }

    $result = UnifiedLicensing::validateLicense([
        'api_key'     => get_option( 'myplugin_api_key' ),
        'product_key' => 'prod_my-awesome-plugin',
        'license_key' => $license_key,
        'machine_id'  => wp_parse_url( home_url(), PHP_URL_HOST ),
        'platform'    => 'php',
    ]);

    update_option(
        'myplugin_license_status',
        ! empty( $result['valid'] ) ? 'valid' : 'invalid'
    );
}
register_activation_hook( __FILE__, 'myplugin_activate' );
Why store status in wp_options? Activation hooks can't render UI, so we persist a simple status string (valid, invalid, missing) and let the admin notice system read it later. Cheap, reliable, and survives cache clears.

Reading keys from wp_options

We lean on three options throughout this guide. Define them once and reuse them everywhere:

Option nameHolds
myplugin_api_keyYour account API key (ul_live_…)
myplugin_product_keyThe product identifier (prod_…)
myplugin_license_keyThe customer's license key

Tip: bake the API key and product key into your plugin as constants if they're fixed per release — then customers only ever need to enter their license key.

04Settings Page

Customers need somewhere to paste their license key. The native WordPress Settings API is perfect: add_options_page creates the screen, register_setting handles sanitization and storage.

Register the page and settings

phpadd_action( 'admin_menu', 'myplugin_add_settings_page' );
add_action( 'admin_init', 'myplugin_register_settings' );

function myplugin_add_settings_page() {
    add_options_page(
        'My Plugin License',          // Page title
        'My Plugin License',          // Menu label
        'manage_options',              // Capability required
        'myplugin-license',            // Menu slug
        'myplugin_render_settings_page'
    );
}

function myplugin_register_settings() {
    register_setting( 'myplugin_license_group', 'myplugin_api_key',     [ 'sanitize_callback' => 'sanitize_text_field' ] );
    register_setting( 'myplugin_license_group', 'myplugin_product_key', [ 'sanitize_callback' => 'sanitize_text_field' ] );
    register_setting( 'myplugin_license_group', 'myplugin_license_key',  [ 'sanitize_callback' => 'sanitize_text_field' ] );
}

Render the form

phpfunction myplugin_render_settings_page() {
    if ( ! current_user_can( 'manage_options' ) ) {
        return;
    }
    $api_key     = get_option( 'myplugin_api_key', '' );
    $product_key = get_option( 'myplugin_product_key', '' );
    $license_key = get_option( 'myplugin_license_key', '' );
    ?>
    <div class="wrap">
        <h1>My Plugin — License</h1>

        <form method="post" action="options.php">
            <?php settings_fields( 'myplugin_license_group' ); ?>
            <?php do_settings_sections( 'myplugin-license' ); ?>

            <table class="form-table">
                <tr>
                    <th><label for="api_key">API Key</label></th>
                    <td><input type="text" id="api_key"
                        name="myplugin_api_key"
                        value="<?php echo esc_attr( $api_key ); ?>"
                        class="regular-text"></td>
                </tr>
                <tr>
                    <th><label for="product_key">Product Key</label></th>
                    <td><input type="text" id="product_key"
                        name="myplugin_product_key"
                        value="<?php echo esc_attr( $product_key ); ?>"
                        class="regular-text"></td>
                </tr>
                <tr>
                    <th><label for="license_key">License Key</label></th>
                    <td><input type="text" id="license_key"
                        name="myplugin_license_key"
                        value="<?php echo esc_attr( $license_key ); ?>"
                        class="regular-text"
                        placeholder="XXXX-XXXX-XXXX-XXXX"></td>
                </tr>
            </table>

            <?php submit_button( 'Save License' ); ?>
        </form>
    </div>
    <?php
}
Simpler path: if the API key and product key never change per customer, define them as constants in the plugin file and show only the license key field. Fewer fields, fewer support tickets.

05License Check on Every Load

Activation is one moment in time — licenses can expire or be revoked later. Hooking wp_loaded lets us re-validate on every admin request and surface problems immediately.

phpadd_action( 'wp_loaded', 'myplugin_check_license' );

function myplugin_check_license() {
    // Only police the admin area — don't slow down the front end.
    if ( ! is_admin() || wp_doing_ajax() ) {
        return;
    }

    // Cache the result for 12 hours so we don't ping the API every pageload.
    $cached = get_transient( 'myplugin_license_valid' );
    if ( 'yes' === $cached ) {
        return; // Recently verified, all good.
    }

    $license_key = get_option( 'myplugin_license_key', '' );
    if ( empty( $license_key ) ) {
        add_action( 'admin_notices', 'myplugin_license_notice_missing' );
        return;
    }

    $result = UnifiedLicensing::validateLicense([
        'api_key'     => get_option( 'myplugin_api_key' ),
        'product_key' => get_option( 'myplugin_product_key' ),
        'license_key' => $license_key,
        'machine_id'  => wp_parse_url( home_url(), PHP_URL_HOST ),
        'platform'    => 'php',
    ]);

    if ( ! empty( $result['valid'] ) ) {
        set_transient( 'myplugin_license_valid', 'yes', 12 * HOUR_IN_SECONDS );
        return;
    }

    // Invalid or expired — clear the cache and raise the alarm.
    delete_transient( 'myplugin_license_valid' );
    add_action( 'admin_notices', 'myplugin_license_notice_invalid' );
}

function myplugin_license_notice_missing() {
    printf(
        '<div class="notice notice-warning"><p><strong>%s</strong> %s <a href="%s">%s</a></p></div>',
        esc_html__( 'Almost there!', 'my-plugin' ),
        esc_html__( 'Enter your license key to unlock updates and premium features.', 'my-plugin' ),
        esc_url( admin_url( 'options-general.php?page=myplugin-license' ) ),
        esc_html__( 'Activate now →', 'my-plugin' )
    );
}

function myplugin_license_notice_invalid() {
    printf(
        '<div class="notice notice-error"><p><strong>%s</strong> %s <a href="%s">%s</a></p></div>',
        esc_html__( 'License problem.', 'my-plugin' ),
        esc_html__( 'Your license key is invalid or has expired.', 'my-plugin' ),
        esc_url( admin_url( 'options-general.php?page=myplugin-license' ) ),
        esc_html__( 'Review your license →', 'my-plugin' )
    );
}
Be kind to the API (and your users). The 12-hour transient above means one validation per half-day per site, not one per pageload. If the API is briefly unreachable, treat it as "still valid" rather than locking people out — fail open on network errors, fail closed on definitive rejections.

06AJAX Activation

A save-and-refresh form works, but instant feedback feels magical. Let's add an AJAX activation form using WordPress's admin-ajax.php.

The server side

phpadd_action( 'wp_ajax_myplugin_activate_license', 'myplugin_ajax_activate_license' );

function myplugin_ajax_activate_license() {

    // Security first: verify nonce + capability.
    check_ajax_referer( 'myplugin_activate', 'nonce' );

    if ( ! current_user_can( 'manage_options' ) ) {
        wp_send_json_error( [ 'message' => 'Permission denied.' ], 403 );
    }

    $license_key = sanitize_text_field( wp_unslash( $_POST['license_key'] ?? '' ) );
    $api_key     = get_option( 'myplugin_api_key' );
    $product_key = get_option( 'myplugin_product_key' );

    if ( empty( $license_key ) || empty( $api_key ) || empty( $product_key ) ) {
        wp_send_json_error( [ 'message' => 'Please fill in all license fields.' ] );
    }

    $result = UnifiedLicensing::validateLicense([
        'api_key'     => $api_key,
        'product_key' => $product_key,
        'license_key' => $license_key,
        'machine_id'  => wp_parse_url( home_url(), PHP_URL_HOST ),
        'platform'    => 'php',
    ]);

    if ( empty( $result['valid'] ) ) {
        wp_send_json_error([
            'message' => $result['error'] ?? 'That license key could not be validated.',
        ]);
    }

    // Success — persist everything and clear the stale-status cache.
    update_option( 'myplugin_license_key', $license_key );
    update_option( 'myplugin_license_status', 'valid' );
    set_transient( 'myplugin_license_valid', 'yes', 12 * HOUR_IN_SECONDS );

    wp_send_json_success([ 'message' => 'License activated. Thanks for your support! 🎉' ]);
}

The frontend form + JS

Drop this into your settings page (or anywhere in the admin):

html<div id="ul-license-box">
    <input type="text" id="ul-license-input" placeholder="XXXX-XXXX-XXXX-XXXX">
    <button type="button" id="ul-license-submit">Activate License</button>
    <p id="ul-license-message" aria-live="polite"></p>
</div>
js// admin/js/license-activation.js
(function () {
    const button  = document.getElementById('ul-license-submit');
    const input   = document.getElementById('ul-license-input');
    const message = document.getElementById('ul-license-message');

    button.addEventListener('click', async () => {
        message.textContent = 'Validating…';
        button.disabled = true;

        const body = new FormData();
        body.append('action',      'myplugin_activate_license');
        body.append('nonce',       UL_LICENSE.nonce);
        body.append('license_key', input.value.trim());

        try {
            const res = await fetch(UL_LICENSE.ajaxUrl, { method: 'POST', body });
            const json = await res.json();

            message.textContent = json.data.message;
            message.style.color = json.success ? '#7fb069' : '#d96c5f';
        } catch (err) {
            message.textContent = 'Network error — please try again.';
            message.style.color = '#d96c5f';
        } finally {
            button.disabled = false;
        }
    });
})();

Enqueue and localize

phpadd_action( 'admin_enqueue_scripts', 'myplugin_enqueue_license_js' );

function myplugin_enqueue_license_js( $hook ) {
    if ( 'settings_page_myplugin-license' !== $hook ) {
        return;
    }

    wp_enqueue_script(
        'myplugin-license-activation',
        plugin_dir_url( __FILE__ ) . 'admin/js/license-activation.js',
        [],
        '1.0.0',
        true
    );

    wp_localize_script( 'myplugin-license-activation', 'UL_LICENSE', [
        'ajaxUrl' => admin_url( 'admin-ajax.php' ),
        'nonce'   => wp_create_nonce( 'myplugin_activate' ),
    ]);
}
How the pieces connect: the JS posts action=myplugin_activate_license to admin-ajax.php; WordPress routes it to our handler because of the wp_ajax_{action} hook; the nonce proves the request came from our own page.

07WooCommerce Integration

Selling through WooCommerce? Two natural integration points: validating a license supplied at checkout, and generating/delivering licenses after purchase.

Validate a license at checkout

If customers enter their existing license key during checkout (common for upgrades or seat additions), validate it before the order goes through:

phpadd_action( 'woocommerce_checkout_process', 'myplugin_validate_checkout_license' );

function myplugin_validate_checkout_license() {
    // Only when our custom checkout field was filled in.
    if ( empty( $_POST['billing_license_key'] ) ) {
        return;
    }

    $license_key = sanitize_text_field( wp_unslash( $_POST['billing_license_key'] ) );

    $result = UnifiedLicensing::validateLicense([
        'api_key'     => MYPLUGIN_API_KEY,
        'product_key' => MYPLUGIN_PRODUCT_KEY,
        'license_key' => $license_key,
        'machine_id'  => wp_parse_url( home_url(), PHP_URL_HOST ),
        'platform'    => 'php',
    ]);

    if ( empty( $result['valid'] ) ) {
        // Block checkout with a friendly error.
        wc_add_notice(
            'That license key could not be verified. Please double-check it and try again.',
            'error'
        );
    }
}

Add the matching checkout field:

phpadd_action( 'woocommerce_after_order_notes', 'myplugin_checkout_license_field' );

function myplugin_checkout_license_field( $checkout ) {
    woocommerce_form_field( 'billing_license_key', [
        'type'        => 'text',
        'label'       => 'License Key (optional)',
        'placeholder' => 'XXXX-XXXX-XXXX-XXXX',
        'required'    => false,
    ], $checkout->get_value( 'billing_license_key' ) );
}

WooCommerce settings tab

To manage your API credentials inside WooCommerce itself, extend its settings API:

phpadd_filter( 'woocommerce_get_settings_pages', 'myplugin_wc_settings_page' );

function myplugin_wc_settings_page( $pages ) {
    $pages[] = new MyPlugin_WC_License_Settings();
    return $pages;
}

class MyPlugin_WC_License_Settings extends WC_Settings_Page {
    public function __construct() {
        $this->id    = 'myplugin_license';
        $this->label = __( 'Plugin License', 'my-plugin' );
        parent::__construct();
    }

    public function get_settings( $current_section = '' ) {
        return apply_filters( 'myplugin_wc_settings', [
            [
                'title' => __( 'UnifiedLicensing', 'my-plugin' ),
                'type'  => 'title',
                'desc'  => 'Credentials used to validate licenses at checkout.',
                'id'    => 'myplugin_wc_license_title',
            ],
            [
                'title' => __( 'API Key', 'my-plugin' ),
                'type'  => 'text',
                'id'    => 'myplugin_api_key',
            ],
            [
                'title' => __( 'Product Key', 'my-plugin' ),
                'type'  => 'text',
                'id'    => 'myplugin_product_key',
            ],
            [ 'type' => 'sectionend', 'id' => 'myplugin_wc_license_title' ],
        ] );
    }
}
Fulfillment idea: hook woocommerce_payment_complete to mint a fresh license via the UnifiedLicensing API and email it to the buyer — turning your store into a fully automated license vending machine.

08Trial Support

Trial mode lowers the barrier to trying your plugin. The pattern: start a trial once, store the returned trial_token in wp_options, and check expiry locally thereafter.

Start a trial

phpfunction myplugin_start_trial() {
    // One trial per site — don't restart if one already exists.
    if ( get_option( 'myplugin_trial_token' ) ) {
        return new WP_Error( 'trial_exists', 'A trial has already been started on this site.' );
    }

    $response = wp_remote_post( 'https://api.unifiedlicensing.com/api/v1/start-trial', [
        'timeout' => 15,
        'headers' => [ 'Content-Type' => 'application/json' ],
        'body'    => wp_json_encode([
            'api_key'     => get_option( 'myplugin_api_key' ),
            'product_key' => get_option( 'myplugin_product_key' ),
            'machine_id'  => wp_parse_url( home_url(), PHP_URL_HOST ),
            'platform'    => 'php',
        ]),
    ]);

    if ( is_wp_error( $response ) ) {
        return $response;
    }

    $data = json_decode( wp_remote_retrieve_body( $response ), true );

    if ( empty( $data['trial_token'] ) ) {
        return new WP_Error( 'trial_failed', $data['error'] ?? 'Could not start trial.' );
    }

    // Persist the token and the moment the trial ends.
    update_option( 'myplugin_trial_token', sanitize_text_field( $data['trial_token'] ) );
    update_option( 'myplugin_trial_expires', time() + ( $data['trial_days'] ?? 14 ) * DAY_IN_SECONDS );

    return true;
}

Check trial status

phpfunction myplugin_trial_is_active() {
    $token   = get_option( 'myplugin_trial_token' );
    $expires = (int) get_option( 'myplugin_trial_expires', 0 );

    return ! empty( $token ) && time() < $expires;
}

// Feature gating helper — licensed OR trialing counts as unlocked.
function myplugin_is_unlocked() {
    return get_transient( 'myplugin_license_valid' ) === 'yes'
        || myplugin_trial_is_active();
}
Tamper note: local expiry checks are convenient but not cryptographically enforced — a curious user could edit wp_options. For high-value features, re-validate the trial_token against the API periodically instead of trusting the stored timestamp alone.

09Complete Plugin

Here's everything assembled into one copy-paste-ready plugin file: activation check, settings page, AJAX activation form, and feature gating. Save it as my-plugin/my-plugin.php, put the SDK in includes/, and activate.

php<?php
/**
 * Plugin Name: My Awesome Plugin
 * Description: A demo plugin with full UnifiedLicensing integration.
 * Version:     1.0.0
 * Author:      You
 */

// Bail if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

define( 'MYPLUGIN_VERSION', '1.0.0' );
define( 'MYPLUGIN_PRODUCT_KEY', 'prod_my-awesome-plugin' );

require_once plugin_dir_path( __FILE__ ) . 'includes/UnifiedLicensing.PHP.v4.0-Complete.php';

/* -----------------------------------------------------------------
 * Helpers
 * --------------------------------------------------------------- */

function myplugin_machine_id() {
    return wp_parse_url( home_url(), PHP_URL_HOST ) ?: 'unknown-site';
}

function myplugin_call_validate( $license_key ) {
    return UnifiedLicensing::validateLicense([
        'api_key'     => get_option( 'myplugin_api_key', '' ),
        'product_key' => MYPLUGIN_PRODUCT_KEY,
        'license_key' => $license_key,
        'machine_id'  => myplugin_machine_id(),
        'platform'    => 'php',
    ]);
}

/**
 * Master feature gate: valid license OR active trial unlocks everything.
 */
function myplugin_is_premium_active() {
    if ( get_transient( 'myplugin_license_valid' ) === 'yes' ) {
        return true;
    }

    $token   = get_option( 'myplugin_trial_token' );
    $expires = (int) get_option( 'myplugin_trial_expires', 0 );

    return ! empty( $token ) && time() < $expires;
}

/* -----------------------------------------------------------------
 * Activation / deactivation
 * --------------------------------------------------------------- */

function myplugin_activate() {
    $key = get_option( 'myplugin_license_key', '' );

    if ( empty( $key ) ) {
        update_option( 'myplugin_license_status', 'missing' );
        return;
    }

    $result = myplugin_call_validate( $key );
    update_option(
        'myplugin_license_status',
        ! empty( $result['valid'] ) ? 'valid' : 'invalid'
    );
}
register_activation_hook( __FILE__, 'myplugin_activate' );

function myplugin_deactivate() {
    delete_transient( 'myplugin_license_valid' );
}
register_deactivation_hook( __FILE__, 'myplugin_deactivate' );

/* -----------------------------------------------------------------
 * Periodic license check + admin notices
 * --------------------------------------------------------------- */

add_action( 'wp_loaded', 'myplugin_check_license' );

function myplugin_check_license() {
    if ( ! is_admin() || wp_doing_ajax() ) {
        return;
    }

    if ( get_transient( 'myplugin_license_valid' ) === 'yes' ) {
        return;
    }

    $key = get_option( 'myplugin_license_key', '' );

    if ( empty( $key ) ) {
        add_action( 'admin_notices', 'myplugin_notice_missing' );
        return;
    }

    $result = myplugin_call_validate( $key );

    if ( ! empty( $result['valid'] ) ) {
        set_transient( 'myplugin_license_valid', 'yes', 12 * HOUR_IN_SECONDS );
        return;
    }

    add_action( 'admin_notices', 'myplugin_notice_invalid' );
}

function myplugin_notice_missing() {
    printf(
        '<div class="notice notice-warning"><p>%s <a href="%s">%s</a></p></div>',
        esc_html__( 'My Awesome Plugin: enter your license key to enable premium features.' ),
        esc_url( admin_url( 'options-general.php?page=myplugin-license' ) ),
        esc_html__( 'Activate now →' )
    );
}

function myplugin_notice_invalid() {
    printf(
        '<div class="notice notice-error"><p>%s <a href="%s">%s</a></p></div>',
        esc_html__( 'My Awesome Plugin: your license is invalid or expired.' ),
        esc_url( admin_url( 'options-general.php?page=myplugin-license' ) ),
        esc_html__( 'Fix it →' )
    );
}

/* -----------------------------------------------------------------
 * Settings page
 * --------------------------------------------------------------- */

add_action( 'admin_menu', 'myplugin_admin_menu' );
add_action( 'admin_init', 'myplugin_register_settings' );

function myplugin_admin_menu() {
    add_options_page(
        'My Awesome Plugin License',
        'My Plugin License',
        'manage_options',
        'myplugin-license',
        'myplugin_render_settings'
    );
}

function myplugin_register_settings() {
    register_setting( 'myplugin_license_group', 'myplugin_api_key',     [ 'sanitize_callback' => 'sanitize_text_field' ] );
    register_setting( 'myplugin_license_group', 'myplugin_product_key', [ 'sanitize_callback' => 'sanitize_text_field' ] );
    register_setting( 'myplugin_license_group', 'myplugin_license_key',  [ 'sanitize_callback' => 'sanitize_text_field' ] );
}

function myplugin_render_settings() {
    if ( ! current_user_can( 'manage_options' ) ) {
        return;
    }

    $status = get_option( 'myplugin_license_status', 'missing' );
    $badge  = [
        'valid'   => '✅ Licensed',
        'invalid' => '❌ Invalid',
        'missing' => '⏳ Not activated',
    ][ $status ] ?? '—';
    ?>
    <div class="wrap">
        <h1>My Awesome Plugin — License <code><?php echo esc_html( $badge ); ?></code></h1>

        <form method="post" action="options.php">
            <?php settings_fields( 'myplugin_license_group' ); ?>
            <table class="form-table">
                <tr>
                    <th><label for="mp-api-key">API Key</label></th>
                    <td><input type="text" id="mp-api-key" name="myplugin_api_key"
                        value="<?php echo esc_attr( get_option( 'myplugin_api_key', '' ) ); ?>"
                        class="regular-text"></td>
                </tr>
                <tr>
                    <th><label for="mp-license-key">License Key</label></th>
                    <td><input type="text" id="mp-license-key" name="myplugin_license_key"
                        value="<?php echo esc_attr( get_option( 'myplugin_license_key', '' ) ); ?>"
                        class="regular-text" placeholder="XXXX-XXXX-XXXX-XXXX"></td>
                </tr>
            </table>
            <?php submit_button( 'Save' ); ?>
        </form>

        <hr>
        <h2>Quick Activate</h2>
        <p><?php esc_html_e( 'Paste your key and activate instantly — no page reload.' ); ?></p>
        <input type="text" id="ul-license-input" placeholder="XXXX-XXXX-XXXX-XXXX">
        <button type="button" class="button button-primary" id="ul-license-submit">Activate</button>
        <p id="ul-license-message" aria-live="polite"></p>
    </div>
    <?php
}

/* -----------------------------------------------------------------
 * AJAX activation endpoint
 * --------------------------------------------------------------- */

add_action( 'wp_ajax_myplugin_activate_license', 'myplugin_ajax_activate' );

function myplugin_ajax_activate() {
    check_ajax_referer( 'myplugin_activate', 'nonce' );

    if ( ! current_user_can( 'manage_options' ) ) {
        wp_send_json_error( [ 'message' => 'Permission denied.' ], 403 );
    }

    $key = sanitize_text_field( wp_unslash( $_POST['license_key'] ?? '' ) );

    if ( empty( $key ) ) {
        wp_send_json_error( [ 'message' => 'Enter a license key first.' ] );
    }

    $result = myplugin_call_validate( $key );

    if ( empty( $result['valid'] ) ) {
        wp_send_json_error([
            'message' => $result['error'] ?? 'Validation failed — check the key and try again.',
        ]);
    }

    update_option( 'myplugin_license_key', $key );
    update_option( 'myplugin_license_status', 'valid' );
    set_transient( 'myplugin_license_valid', 'yes', 12 * HOUR_IN_SECONDS );

    wp_send_json_success([ 'message' => 'License activated — premium features unlocked!' ]);
}

add_action( 'admin_enqueue_scripts', 'myplugin_enqueue_assets' );

function myplugin_enqueue_assets( $hook ) {
    if ( 'settings_page_myplugin-license' !== $hook ) {
        return;
    }

    wp_register_script( 'myplugin-license', false, [], MYPLUGIN_VERSION, true );
    wp_enqueue_script( 'myplugin-license' );

    $js = <<<JS
document.addEventListener('DOMContentLoaded', function () {
    var btn = document.getElementById('ul-license-submit');
    var input = document.getElementById('ul-license-input');
    var msg = document.getElementById('ul-license-message');
    if (!btn) return;
    btn.addEventListener('click', async function () {
        msg.textContent = 'Validating…';
        btn.disabled = true;
        var body = new FormData();
        body.append('action', 'myplugin_activate_license');
        body.append('nonce', UL_LICENSE.nonce);
        body.append('license_key', input.value.trim());
        try {
            var res = await fetch(UL_LICENSE.ajaxUrl, { method: 'POST', body: body });
            var json = await res.json();
            msg.textContent = json.data.message;
            msg.style.color = json.success ? '#7fb069' : '#d96c5f';
        } catch (e) {
            msg.textContent = 'Network error — please try again.';
            msg.style.color = '#d96c5f';
        } finally {
            btn.disabled = false;
        }
    });
});
JS;

    wp_add_inline_script( 'myplugin-license', $js );
    wp_localize_script( 'myplugin-license', 'UL_LICENSE', [
        'ajaxUrl' => admin_url( 'admin-ajax.php' ),
        'nonce'   => wp_create_nonce( 'myplugin_activate' ),
    ]);
}

/* -----------------------------------------------------------------
 * Example feature gating
 * --------------------------------------------------------------- */

add_shortcode( 'myplugin_premium', 'myplugin_premium_shortcode' );

function myplugin_premium_shortcode( $atts, $content = '' ) {
    if ( myplugin_is_premium_active() ) {
        return do_shortcode( $content );
    }
    return '<em>This content requires an active license.</em> '
         . wp_kses_post( sprintf(
             '<a href="%s">Activate here</a>.',
             admin_url( 'options-general.php?page=myplugin-license' )
         ) );
}
Usage: wrap any premium content in [myplugin_premium]…[/myplugin_premium] — it renders only when a valid license or active trial exists. Swap the shortcode for whatever gating makes sense in your plugin: filters, REST permission callbacks, template conditionals, you name it.

11Security Hardening

WordPress plugins run PHP on the customer's server — they have full access to your code. No check is uncrackable, but you can make piracy annoying enough that most people give up and just pay.

11.1 — Periodic Re-validation (not just on activate)

The activation hook fires once. After that, a pirate can delete the check entirely. Force re-validation every 24 hours via wp_cron:

php// Schedule daily re-validation on activation
function myplugin_schedule_license_check() {
    if ( ! wp_next_scheduled( 'myplugin_daily_license_check' ) ) {
        wp_schedule_event( time(), 'daily', 'myplugin_daily_license_check' );
    }
}
register_activation_hook( __FILE__, 'myplugin_schedule_license_check' );

function myplugin_daily_license_check() {
    $key = get_option( 'myplugin_license_key' );
    if ( ! $key ) return;

    $result = UnifiedLicensing::validateLicense([
        'api_key'     => get_option( 'myplugin_api_key' ),
        'product_key' => 'prod_my-awesome-plugin',
        'license_key' => $key,
        'machine_id'  => wp_parse_url( home_url(), PHP_URL_HOST ),
        'platform'    => 'php',
    ]);

    update_option(
        'myplugin_license_status',
        ! empty( $result['valid'] ) ? 'valid' : 'invalid'
    );
}
add_action( 'myplugin_daily_license_check', 'myplugin_daily_license_check' );
Note: A determined pirate can disable wp_cron entirely or delete the scheduled event. This raises the bar — it doesn't set it in stone. The goal is casual pirates, not your most determined attacker.

11.2 — Feature Gating (check on every request, not just activation)

Don't check the license once and forget it. Gate premium features on every load:

phpfunction myplugin_is_premium_active() {
    $status = get_option( 'myplugin_license_status', 'missing' );
    return $status === 'valid';
}

// Use everywhere you gate features
if ( ! myplugin_is_premium_active() ) {
    return; // or show upgrade prompt
}

Pirate deletes the myplugin_is_premium_active() function? Every call breaks — the whole plugin becomes unusable. They'd have to rewrite your feature gating from scratch.

11.3 — Obfuscate (don't store keys in plain text)

The myplugin_license_key option is stored as plain text in wp_options. Anyone with DB access can copy it. At minimum, hash what you don't need to read back:

php// Store a hash for quick validation (you can't reverse this)
$key_hash = hash( 'sha256', $license_key );
update_option( 'myplugin_license_hash', $key_hash );

// For the actual key (needed for API calls), encrypt it
$encrypted = openssl_encrypt(
    $license_key,
    'aes-256-cbc',
    wp_salt(),          // encryption key from wp-config
    0,
    substr( md5( wp_salt() ), 0, 16 )  // IV
);
update_option( 'myplugin_license_enc', $encrypted );

11.4 — Decoy Options (poison the well)

Add fake options that look real but break things when modified:

php// On activation, plant a decoy
update_option( 'myplugin_license_validator', wp_generate_password'( 32, false ) );

// Check it on every validation — if someone "fixes" your license
// by editing wp_options, this breaks their change silently
function myplugin_check_integrity() {
    $validator = get_option( 'myplugin_license_validator' );
    $expected  = wp_salt() . 'myplugin';

    if ( $validator !== hash( 'sha256', $expected ) ) {
        // Someone tampered — silently fail
        update_option( 'myplugin_license_status', 'invalid' );
    }
}
add_action( 'wp_loaded', 'myplugin_check_integrity' );

11.5 — Server-Side Features (the real moat)

The strongest protection: don't put premium logic in the plugin at all. Keep it on your server:

php// Plugin sends a request to YOUR API, YOUR server decides
$result = wp_remote_post( 'https://api.yourplugin.com/premium-feature', [
    'body' => [
        'license_key' => get_option( 'myplugin_license_key' ),
        'action'     => 'generate-report',
    ],
]);

// If license is invalid, YOUR server refuses to respond.
// No server-side code = no premium feature.
This is what WooCommerce, Gravity Forms, and Astra Pro do. The plugin is free/open, but premium features call home to the vendor's API. Pirated copies can't fake the server response because they never reach your backend.

11.6 — Summary: Defense in Depth

LayerWhat it stopsEffort to bypass
Activation checkCasual piracy (copy-paste)Trivial — edit one line
Daily re-validationPirates who forget to re-checkDisable wp_cron or remove hook
Feature gating functionSimple deletion of checksRewrite all gate calls
Encrypted storageDB key extractionDecrypt with wp_salt
Decoy optionsNaive wp_options editsFind the decoy, remove it
Server-side featuresAll of the aboveReverse-engineer your API

Stack as many layers as your product's value justifies. For a $10 plugin, activation check + daily re-validation is usually enough. For a $200 plugin, add server-side features. For a $500+ enterprise tool, server-side is mandatory.

12Troubleshooting

When things go sideways, nine times out of ten it's one of these:

"cURL error 60: SSL certificate problem"

Common on local dev environments (Local by Flywheel, XAMPP) with outdated CA bundles. Update your CA certificate bundle, or for local development only, point WP HTTP at a fresh cacert.pem. Never disable SSL verification in production.

"Invalid api_key"

"Invalid product_key"

The product_key must match a product registered in your UnifiedLicensing dashboard exactly — including the prod_ prefix and casing.

License validates but features stay locked

You're probably reading the cached transient. After activating via AJAX the transient is refreshed automatically, but if you edited wp_options directly, delete the myplugin_license_valid transient (or just deactivate/reactivate the plugin).

AJAX returns 400 "Bad Request"

Works on one site, fails on another

Licenses are often bound to machine_id. We use the site hostname — if the site moved domains (staging → production), deactivate the old seat in the dashboard or re-issue the license for the new domain.

Outbound requests blocked

Some hosts firewall outbound HTTPS from PHP. Verify with:

php$res = wp_remote_get( 'https://api.unifiedlicensing.com/api/v1/health' );
var_dump( is_wp_error( $res ) ? $res->get_error_message() : wp_remote_retrieve_response_code( $res ) );

A 200 means connectivity is fine and the issue is in your request payload. Anything else — talk to your host about allowlisting api.unifiedlicensing.com.

Still stuck?

Enable WP debug logging, capture the exact request/response pair, and reach out via the support channels on the guides index. Include the plugin version, PHP version, and the raw error message — it makes diagnosis dramatically faster.