Tutorial

How to Add License Key Validation to Your WordPress Plugin

TOT
Traffic Orchestrator Team
Engineering
May 22, 2026 12 min read 1,940 words
Share

WordPress plugin developers face a hard choice when it comes to licensing. WP-native solutions like Easy Digital Downloads and WooCommerce lock you into the WordPress ecosystem. Homegrown validation code becomes a maintenance burden and a security risk. SaaS licensing platforms take revenue cuts or charge per-seat. API-first licensing gives you a fourth option: validate license keys against a purpose-built service, sell through any payment provider you want, and keep your plugin's licensing logic under 50 lines of PHP.

Why API-First Licensing for WordPress Plugins

Traditional WordPress licensing ties your plugin to a specific commerce platform. If you sell through WooCommerce today, migrating to Stripe or Gumroad later means rewriting your entire licensing layer. API-first licensing decouples validation from commerce:

  • Payment portability — Sell via WooCommerce, Gumroad, Stripe, Paddle, or LemonSqueezy. Your license validation doesn’t change.
  • Separation of concerns — Your plugin validates keys. Your payment provider handles billing. Traffic Orchestrator handles the licensing infrastructure.
  • Global edge validation — License checks resolve at 300+ edge locations in under 10ms. Your customers’ sites stay fast regardless of where they’re hosted.
  • Cross-platform future-proofing — If you later ship a Shopify app, a SaaS version, or a standalone desktop tool, the same licensing API covers all of them.

Prerequisites

Before starting, you’ll need:

  • A Traffic Orchestrator account (the free Builder plan includes 5 licenses and 500 validations/month)
  • A WordPress plugin (new or existing) that you want to protect with license keys
  • PHP 7.4+ and Composer installed in your development environment
  • A product created in your Traffic Orchestrator dashboard with at least one license key generated
No Composer? If your plugin doesn’t use Composer, you can call the REST API directly with wp_remote_post(). We’ll show both approaches. The SDK approach is recommended because it handles caching, retries, and offline fallback automatically.

Step 1 — Install the SDK

Add the Traffic Orchestrator PHP SDK to your plugin’s dependencies:

composer require traffic-orchestrator/sdk

If your plugin bundles its own vendor/ directory (most premium plugins do), the SDK is included automatically. The package requires PHP 7.4+ and has zero external dependencies beyond PSR-18 HTTP client interfaces.

WordPress-specific wrapper: Traffic Orchestrator also provides a WordPress-specific package that integrates with WordPress HTTP API and admin UI patterns. Install it with composer require traffic-orchestrator/sdk and use the TrafficOrchestratorWordPress namespace for WordPress-aware helpers.

Step 2 — Configure the Client

Initialize the Traffic Orchestrator client in your plugin’s main file. The client needs your API key (stored as a WordPress option or defined as a constant) and the product identifier:

<?php
// my-plugin/includes/class-license.php

use TrafficOrchestratorClient;
use TrafficOrchestratorConfig;

final class MyPlugin_License {

    private static ?Client $client = null;

    public static function getClient(): Client {
        if (self::$client === null) {
            self::$client = new Client(new Config([
                'apiKey'  => defined('MY_PLUGIN_TO_API_KEY')
                    ? MY_PLUGIN_TO_API_KEY
                    : get_option('my_plugin_api_key', ''),
                'product' => 'my-wordpress-plugin',
                'baseUrl' => 'https://api.trafficorchestrator.com',
            ]));
        }

        return self::$client;
    }
}

The product field must match the product slug you created in the Traffic Orchestrator dashboard. The apiKey is your account-level API key — distinct from the license keys your customers enter.

Step 3 — Add License Validation

The core validation call checks whether a customer’s license key is valid for their WordPress domain. This is the function you’ll call from your admin page, update checker, and feature gates:

/**
 * Validate a license key against the current domain.
 *
 * @return array{valid: bool, plan: string, features: string[], expiresAt: string}
 */
public static function validate(): array {
    $licenseKey = get_option('my_plugin_license_key', '');
    if (empty($licenseKey)) {
        return ['valid' => false, 'plan' => '', 'features' => [], 'expiresAt' => ''];
    }

    // Check transient cache first (avoid hitting the API on every page load)
    $cached = get_transient('my_plugin_license_result');
    if ($cached !== false) {
        return $cached;
    }

    $result = self::getClient()->validate([
        'key'    => $licenseKey,
        'domain' => wp_parse_url(home_url(), PHP_URL_HOST),
    ]);

    // Cache for 12 hours on success, 1 hour on failure
    $ttl = $result['valid'] ? 12 * HOUR_IN_SECONDS : HOUR_IN_SECONDS;
    set_transient('my_plugin_license_result', $result, $ttl);

    return $result;
}

Feature Gating for Tiered Plugins

If your plugin has free/pro/business tiers, use the features array returned by the validation response to gate specific capabilities:

// Gate a premium feature behind a license tier
$license = MyPlugin_License::validate();

if (in_array('advanced-analytics', $license['features'], true)) {
    // Render the analytics dashboard
    my_plugin_render_analytics();
} else {
    // Show upgrade prompt
    my_plugin_render_upgrade_notice('advanced-analytics');
}

// Check the plan name directly
if ($license['plan'] === 'business' || $license['plan'] === 'professional') {
    // Enable bulk operations
    my_plugin_enable_bulk_ops();
}
Feature names are configured in your dashboard. When you create a license in Traffic Orchestrator, you assign it a plan and a set of feature flags. The validation response includes these features, so your plugin can make gating decisions without hardcoding tier logic.

Step 4 — Admin License Page

Your customers need a place to enter and manage their license key. Add a settings page under the WordPress admin menu:

// Register the admin page
add_action('admin_menu', function () {
    add_options_page(
        'My Plugin License',
        'My Plugin License',
        'manage_options',
        'my-plugin-license',
        'my_plugin_license_page'
    );
});

// Render the settings page
function my_plugin_license_page() {
    $licenseKey = get_option('my_plugin_license_key', '');
    $result     = MyPlugin_License::validate();
    $status     = $result['valid'] ? 'active' : 'inactive';

    ?>
    <div class="wrap">
        <h1>License Settings</h1>
        <form method="post" action="options.php">
            <?php settings_fields('my_plugin_license_group'); ?>
            <table class="form-table">
                <tr>
                    <th scope="row">License Key</th>
                    <td>
                        <input type="text"
                               name="my_plugin_license_key"
                               value="<?php echo esc_attr($licenseKey); ?>"
                               class="regular-text" />
                    </td>
                </tr>
                <tr>
                    <th scope="row">Status</th>
                    <td>
                        <span class="license-status license-<?php
                            echo esc_attr($status);
                        ?>">
                            <?php echo esc_html(ucfirst($status)); ?>
                        </span>
                        <?php if ($result['valid'] && !empty($result['expiresAt'])): ?>
                            <br><small>Expires:
                                <?php echo esc_html($result['expiresAt']); ?>
                            </small>
                        <?php endif; ?>
                    </td>
                </tr>
                <?php if ($result['valid'] && !empty($result['features'])): ?>
                <tr>
                    <th scope="row">Licensed Features</th>
                    <td><?php echo esc_html(
                        implode(', ', $result['features'])
                    ); ?></td>
                </tr>
                <?php endif; ?>
            </table>
            <?php submit_button('Save License Key'); ?>
        </form>
    </div>
    <?php
}

// Register the setting and clear cache on update
add_action('admin_init', function () {
    register_setting('my_plugin_license_group', 'my_plugin_license_key', [
        'sanitize_callback' => function ($key) {
            // Clear cached validation when the key changes
            delete_transient('my_plugin_license_result');
            return sanitize_text_field($key);
        },
    ]);
});

When a customer saves a new license key, the transient cache is cleared and the next validation call goes directly to the API. The page shows the current status, expiry date, and licensed features.

Step 5 — Gate Updates Behind License

The most effective incentive for license renewal is gating plugin updates behind a valid license. WordPress checks for updates via the update_plugins transient. Hook into this system to serve updates only to licensed installations:

add_filter(
    'pre_set_site_transient_update_plugins',
    'my_plugin_check_updates'
);

function my_plugin_check_updates($transient) {
    if (empty($transient->checked)) {
        return $transient;
    }

    $license = MyPlugin_License::validate();
    if (!$license['valid']) {
        return $transient; // No updates for unlicensed installations
    }

    $remote = wp_remote_get(
        'https://api.trafficorchestrator.com/api/v1/releases/latest',
        [
            'headers' => [
                'Authorization' => 'Bearer ' . get_option('my_plugin_license_key'),
                'X-Domain'      => wp_parse_url(home_url(), PHP_URL_HOST),
            ],
            'timeout' => 10,
        ]
    );

    if (is_wp_error($remote) || wp_remote_retrieve_response_code($remote) !== 200) {
        return $transient;
    }

    $release = json_decode(wp_remote_retrieve_body($remote), true);
    $pluginSlug = plugin_basename(MY_PLUGIN_FILE);
    $currentVersion = $transient->checked[$pluginSlug] ?? '0.0.0';

    if (version_compare($release['version'], $currentVersion, '>')) {
        $transient->response[$pluginSlug] = (object) [
            'slug'        => 'my-plugin',
            'plugin'      => $pluginSlug,
            'new_version' => $release['version'],
            'url'         => $release['changelog_url'] ?? '',
            'package'     => $release['download_url'],
            'tested'      => $release['tested_wp'] ?? '',
            'requires'    => $release['requires_wp'] ?? '5.8',
        ];
    }

    return $transient;
}
Security patches. Consider serving critical security updates even to unlicensed installations. Leaving known vulnerabilities unpatched on customer sites creates liability. Gate feature updates behind licensing, but let security fixes through.

Step 6 — Periodic Re-Validation

A single validation at activation is not sufficient. Licenses expire, get revoked, or change tiers. Use WordPress’s built-in cron system to re-validate daily:

// Schedule the daily license check on plugin activation
register_activation_hook(MY_PLUGIN_FILE, function () {
    if (!wp_next_scheduled('my_plugin_license_check')) {
        wp_schedule_event(time(), 'daily', 'my_plugin_license_check');
    }
});

// Clear the schedule on plugin deactivation
register_deactivation_hook(MY_PLUGIN_FILE, function () {
    wp_clear_scheduled_hook('my_plugin_license_check');
});

// The daily validation handler
add_action('my_plugin_license_check', function () {
    // Force a fresh validation (bypass transient cache)
    delete_transient('my_plugin_license_result');
    $result = MyPlugin_License::validate();

    // Store the last check timestamp for admin display
    update_option('my_plugin_last_license_check', current_time('mysql'));

    if (!$result['valid']) {
        // License is no longer valid — enter grace period
        $graceStart = get_option('my_plugin_grace_period_start');

        if (empty($graceStart)) {
            // First failure: start a 7-day grace period
            update_option('my_plugin_grace_period_start', time());
        } elseif ((time() - (int) $graceStart) > 7 * DAY_IN_SECONDS) {
            // Grace period expired: disable premium features
            update_option('my_plugin_license_status', 'expired');
            // Optionally notify the admin
            my_plugin_send_admin_notice(
                'Your license has expired. Premium features have been disabled.'
            );
        }
    } else {
        // License is valid: clear any grace period
        delete_option('my_plugin_grace_period_start');
        update_option('my_plugin_license_status', 'active');
    }
});

The 7-day grace period prevents a temporary API outage or payment hiccup from immediately breaking your customer’s site. After the grace period, premium features degrade to free-tier behavior rather than deactivating the plugin entirely.

Domain-Bound Licensing

Domain-bound validation is the most effective distribution control mechanism for WordPress plugins. When the validation call includes wp_parse_url(home_url(), PHP_URL_HOST), the API checks that the domain matches the license’s allowed domains list.

This prevents:

  • Key sharing — A license key activated on store-a.com cannot be used on store-b.com without an additional activation
  • Nulled redistribution — Even if someone removes your client-side checks, the API still rejects requests from unauthorized domains
  • Staging abuse — You can configure domain limits to allow production + staging, or charge separately for each

Handling Domain Changes

WordPress sites change domains (migrations, rebrands, staging-to-production pushes). Handle this gracefully:

// Detect domain changes and re-activate
add_action('init', function () {
    $activatedDomain = get_option('my_plugin_activated_domain', '');
    $currentDomain   = wp_parse_url(home_url(), PHP_URL_HOST);

    if (!empty($activatedDomain) && $activatedDomain !== $currentDomain) {
        // Domain changed — deactivate old domain, activate new one
        $client = MyPlugin_License::getClient();
        $key    = get_option('my_plugin_license_key');

        // Deactivate the old domain
        $client->deactivate([
            'key'    => $key,
            'domain' => $activatedDomain,
        ]);

        // Activate the new domain
        $result = $client->activate([
            'key'    => $key,
            'domain' => $currentDomain,
        ]);

        if ($result['valid']) {
            update_option('my_plugin_activated_domain', $currentDomain);
            delete_transient('my_plugin_license_result');
        }
    }
}, 20);

Offline Validation

Some WordPress installations run in environments with unreliable or no internet connectivity — corporate intranets, local development servers, or air-gapped deployments. Traffic Orchestrator supports offline validation using Ed25519 cryptographic signatures.

When a license is issued, the API signs the license payload with an Ed25519 private key. Your plugin can verify this signature locally without making a network request:

use TrafficOrchestratorOfflineValidator;

// One-time: store the signed license token during online activation
$activation = MyPlugin_License::getClient()->activate([
    'key'    => $licenseKey,
    'domain' => wp_parse_url(home_url(), PHP_URL_HOST),
]);

if ($activation['valid'] && !empty($activation['signedToken'])) {
    update_option('my_plugin_signed_token', $activation['signedToken']);
}

// Offline verification (no network required)
$validator = new OfflineValidator([
    'publicKey' => MY_PLUGIN_ED25519_PUBLIC_KEY, // Your Ed25519 public key
]);

$token  = get_option('my_plugin_signed_token', '');
$result = $validator->verify($token);

if ($result['valid']) {
    // Signature is cryptographically valid
    // Check expiration
    if (strtotime($result['expiresAt']) > time()) {
        // License is valid and not expired
    }
}
Offline validation requires the Professional plan or above. The signed token includes the license key, domain, plan, features, and expiration date — all signed with Ed25519. The public key is embedded in your plugin. Tampering with any field invalidates the signature.

Putting It Together

Here’s the full integration checklist for adding license key validation to a WordPress plugin:

StepWhat It DoesWordPress Hook
Install SDKAdds the PHP client librarycomposer require
Configure clientSets API key and productPlugin bootstrap
Validate on activationChecks key + domain on saveadmin_init
Feature gatingGates premium features by planConditional rendering
Admin settings pageLicense key input and status displayadmin_menu
Update gatingServes updates only to licensed sitespre_set_site_transient_update_plugins
Daily re-validationCatches expiry, revocation, tier changeswp_cron
Domain change detectionRe-activates after domain migrationinit
Offline fallbackEd25519 signature verificationConditional (no network)

The complete integration adds approximately 200 lines of PHP to your plugin. The SDK handles HTTP transport, retry logic, response parsing, and cache management. Your plugin code focuses on where to validate and what to gate.

Add License Validation to Your WordPress Plugin

Traffic Orchestrator provides the licensing API, PHP SDK, domain-bound validation, and Ed25519 offline verification. The free Builder plan includes 5 licenses and 500 validations/month — no credit card required.

Create Free Account
TOT
Traffic Orchestrator Team
Engineering

The engineering team behind Traffic Orchestrator, building enterprise-grade software licensing infrastructure used by developers worldwide.

Was this article helpful?
Get licensing insights delivered

Engineering deep-dives, security advisories, and product updates. Unsubscribe anytime.

Share this article
Free Plan Available

Ship licensing in your next release

5 licenses, 500 validations/month, full API access. Set up in under 5 minutes — no credit card required.

2-minute setup No credit card Cancel anytime