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.
Howdy! Let's get your plugin licensed in about five minutes. The whole thing boils down to three moves:
includes/ folder.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/
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';
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.
}
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:
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' );
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.
wp_optionsWe lean on three options throughout this guide. Define them once and reuse them everywhere:
| Option name | Holds |
|---|---|
myplugin_api_key | Your account API key (ul_live_…) |
myplugin_product_key | The product identifier (prod_…) |
myplugin_license_key | The 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.
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.
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' ] );
}
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
}
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' )
);
}
A save-and-refresh form works, but instant feedback feels magical. Let's add an AJAX activation form using WordPress's admin-ajax.php.
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! 🎉' ]);
}
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;
}
});
})();
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' ),
]);
}
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.
Selling through WooCommerce? Two natural integration points: validating a license supplied at checkout, and generating/delivering licenses after purchase.
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' ) );
}
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' ],
] );
}
}
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.
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.
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;
}
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();
}
wp_options. For high-value features, re-validate the trial_token against the API periodically instead of trusting the stored timestamp alone.
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' )
) );
}
[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.
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.
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' );
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.
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.
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 );
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' );
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.
| Layer | What it stops | Effort to bypass |
|---|---|---|
| Activation check | Casual piracy (copy-paste) | Trivial — edit one line |
| Daily re-validation | Pirates who forget to re-check | Disable wp_cron or remove hook |
| Feature gating function | Simple deletion of checks | Rewrite all gate calls |
| Encrypted storage | DB key extraction | Decrypt with wp_salt |
| Decoy options | Naive wp_options edits | Find the decoy, remove it |
| Server-side features | All of the above | Reverse-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.
When things go sideways, nine times out of ten it's one of these:
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.
ul_live_….ul_test_…) don't validate live licenses and vice versa.The product_key must match a product registered in your UnifiedLicensing dashboard exactly — including the prod_ prefix and casing.
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).
check_ajax_referer() action matches wp_create_nonce() exactly.action value in the JS body vs. the wp_ajax_{action} hook name.admin-ajax.php POST responses (rare, but exclude it to be safe).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.
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.
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.