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
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.
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();
}
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;
}
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.comcannot be used onstore-b.comwithout 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
}
}
Putting It Together
Here’s the full integration checklist for adding license key validation to a WordPress plugin:
| Step | What It Does | WordPress Hook |
|---|---|---|
| Install SDK | Adds the PHP client library | composer require |
| Configure client | Sets API key and product | Plugin bootstrap |
| Validate on activation | Checks key + domain on save | admin_init |
| Feature gating | Gates premium features by plan | Conditional rendering |
| Admin settings page | License key input and status display | admin_menu |
| Update gating | Serves updates only to licensed sites | pre_set_site_transient_update_plugins |
| Daily re-validation | Catches expiry, revocation, tier changes | wp_cron |
| Domain change detection | Re-activates after domain migration | init |
| Offline fallback | Ed25519 signature verification | Conditional (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 AccountShip licensing in your next release
5 licenses, 500 validations/month, full API access. Set up in under 5 minutes — no credit card required.