Implement /update-check endpoint aligned with remote OpenAPI spec: - Add checkForUpdates() method to LicenseClientInterface - Add UpdateInfo DTO for update check responses - Add ProductNotFoundException for product_not_found error - Update local openapi.json to v0.4.0 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
31 KiB
Client-Side Implementation Guide
This document provides a comprehensive guide for integrating the WooCommerce Licensed Product Client into existing PHP projects. It covers architecture, classes, configuration, and best practices.
Overview
The magdev/wc-licensed-product-client library provides a complete PHP client for interacting with the WooCommerce Licensed Product REST API. It supports:
- License validation, status checking, and activation
- Response signature verification (HMAC-SHA256)
- PSR-3 logging integration
- PSR-6 caching integration
- Comprehensive exception handling
- Optional code integrity verification
Requirements
- PHP: 8.3 or higher
- Dependencies:
symfony/http-client^7.0psr/log^3.0psr/cache^3.0psr/http-client^1.0
Installation
composer require magdev/wc-licensed-product-client
Architecture
Class Diagram
┌─────────────────────────────┐
│ LicenseClientInterface │
│ (Public API Contract) │
├─────────────────────────────┤
│ + validate() │
│ + status() │
│ + activate() │
│ + checkForUpdates() │
└─────────────┬───────────────┘
│ implements
├───────────────────────┐
▼ ▼
┌─────────────────────┐ ┌──────────────────────┐
│ LicenseClient │ │ SecureLicenseClient │
│ (Basic Client) │ │ (With Signatures) │
└─────────────────────┘ └──────────────────────┘
│ │
└───────────┬───────────┘
▼
┌─────────────────────┐
│ DTOs │
├─────────────────────┤
│ LicenseInfo │
│ LicenseStatus │
│ ActivationResult │
│ UpdateInfo │
│ LicenseState (enum) │
└─────────────────────┘
Directory Structure
src/
├── LicenseClientInterface.php # Public API contract
├── LicenseClient.php # Basic implementation
├── SecureLicenseClient.php # Secure implementation with signatures
├── Dto/
│ ├── LicenseInfo.php # Validation response DTO
│ ├── LicenseStatus.php # Status response DTO + LicenseState enum
│ ├── ActivationResult.php # Activation response DTO
│ └── UpdateInfo.php # Update check response DTO
├── Exception/
│ ├── LicenseException.php # Base exception
│ ├── LicenseNotFoundException.php
│ ├── LicenseExpiredException.php
│ ├── LicenseRevokedException.php
│ ├── LicenseInactiveException.php
│ ├── LicenseInvalidException.php
│ ├── DomainMismatchException.php
│ ├── MaxActivationsReachedException.php
│ ├── ActivationFailedException.php
│ ├── ProductNotFoundException.php
│ └── RateLimitExceededException.php
└── Security/
├── ResponseSignature.php # HMAC signature verification
├── SignatureException.php # Signature verification error
├── StringEncoder.php # XOR-based string obfuscation
├── IntegrityChecker.php # Source file hash verification
└── IntegrityException.php # Integrity check failure
Client Classes
LicenseClientInterface
The public API contract that both client implementations follow:
interface LicenseClientInterface
{
/**
* Validate a license key for a specific domain.
*
* @throws LicenseException When validation fails
*/
public function validate(string $licenseKey, string $domain): LicenseInfo;
/**
* Get detailed status information for a license key.
*
* @throws LicenseException When status check fails
*/
public function status(string $licenseKey): LicenseStatus;
/**
* Activate a license on a specific domain.
*
* @throws LicenseException When activation fails
*/
public function activate(string $licenseKey, string $domain): ActivationResult;
/**
* Check for available plugin updates.
*
* @throws LicenseException When update check fails
*/
public function checkForUpdates(
string $licenseKey,
string $domain,
?string $pluginSlug = null,
?string $currentVersion = null,
): UpdateInfo;
}
LicenseClient (Basic)
The basic client without signature verification. Use this when:
- The server does not support response signing
- You're in a development/testing environment
- Security is handled at another layer
Constructor Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
httpClient |
HttpClientInterface |
Yes | - | Symfony HTTP client instance |
baseUrl |
string |
Yes | - | WordPress site URL (e.g., https://example.com) |
logger |
LoggerInterface|null |
No | NullLogger |
PSR-3 logger for debugging |
cache |
CacheItemPoolInterface|null |
No | null |
PSR-6 cache for responses |
cacheTtl |
int |
No | 300 |
Cache TTL in seconds (5 minutes) |
Example:
use Magdev\WcLicensedProductClient\LicenseClient;
use Symfony\Component\HttpClient\HttpClient;
$client = new LicenseClient(
httpClient: HttpClient::create(),
baseUrl: 'https://your-wordpress-site.com',
);
SecureLicenseClient (Recommended)
The secure client with response signature verification. Use this for production environments.
Constructor Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
httpClient |
HttpClientInterface |
Yes | - | Symfony HTTP client instance |
baseUrl |
string |
Yes | - | WordPress site URL |
serverSecret |
string |
Yes | - | Shared secret (same as server) |
logger |
LoggerInterface|null |
No | NullLogger |
PSR-3 logger |
cache |
CacheItemPoolInterface|null |
No | null |
PSR-6 cache |
cacheTtl |
int |
No | 300 |
Cache TTL in seconds |
verifyIntegrity |
bool |
No | false |
Enable source file integrity checks |
encoder |
StringEncoder|null |
No | Default instance | Custom string encoder |
Example:
use Magdev\WcLicensedProductClient\SecureLicenseClient;
use Symfony\Component\HttpClient\HttpClient;
$client = new SecureLicenseClient(
httpClient: HttpClient::create(),
baseUrl: 'https://your-wordpress-site.com',
serverSecret: 'your-shared-server-secret-min-32-chars',
);
Security Features:
- Response Signature Verification: Every response is verified using HMAC-SHA256
- Timestamp Validation: Prevents replay attacks (5-minute tolerance)
- Per-License Key Derivation: Each license gets a unique signing key
- Obfuscated API Paths: API endpoint paths are XOR-encoded in source code
- Optional Integrity Checking: Verify critical source files haven't been modified
Data Transfer Objects (DTOs)
LicenseInfo
Returned by validate(). Contains license details for a valid license.
final readonly class LicenseInfo
{
public function __construct(
public int $productId, // WooCommerce product ID
public ?\DateTimeImmutable $expiresAt, // null = lifetime license
public ?int $versionId, // Product version (if applicable)
) {}
public static function fromArray(array $data): self;
public function isLifetime(): bool; // Returns true if no expiration
}
Usage:
$info = $client->validate('ABCD-1234-EFGH-5678', 'example.com');
echo "Product ID: " . $info->productId;
echo "Lifetime: " . ($info->isLifetime() ? 'Yes' : 'No');
if (!$info->isLifetime()) {
echo "Expires: " . $info->expiresAt->format('Y-m-d');
}
LicenseStatus
Returned by status(). Contains comprehensive license information.
final readonly class LicenseStatus
{
public function __construct(
public bool $valid, // Is license currently valid
public LicenseState $status, // Status enum value
public string $domain, // Bound domain
public ?\DateTimeImmutable $expiresAt, // Expiration date
public int $activationsCount, // Current activations
public int $maxActivations, // Maximum allowed
) {}
public static function fromArray(array $data): self;
public function isLifetime(): bool;
public function hasAvailableActivations(): bool;
}
Usage:
$status = $client->status('ABCD-1234-EFGH-5678');
echo "Valid: " . ($status->valid ? 'Yes' : 'No');
echo "Status: " . $status->status->value; // active, inactive, expired, revoked
echo "Domain: " . $status->domain;
echo "Activations: " . $status->activationsCount . "/" . $status->maxActivations;
if ($status->hasAvailableActivations()) {
echo "More activations available!";
}
LicenseState (Enum)
Represents possible license states:
enum LicenseState: string
{
case Active = 'active'; // License is active and valid
case Inactive = 'inactive'; // License exists but not activated
case Expired = 'expired'; // License has passed expiration date
case Revoked = 'revoked'; // License was manually revoked
}
ActivationResult
Returned by activate(). Contains activation outcome.
final readonly class ActivationResult
{
public function __construct(
public bool $success, // Whether activation succeeded
public string $message, // Human-readable message
) {}
public static function fromArray(array $data): self;
}
Usage:
$result = $client->activate('ABCD-1234-EFGH-5678', 'newdomain.com');
if ($result->success) {
echo "Activated: " . $result->message;
} else {
echo "Failed: " . $result->message;
}
UpdateInfo
Returned by checkForUpdates(). Contains plugin update information.
final readonly class UpdateInfo
{
public function __construct(
public bool $updateAvailable, // Whether an update is available
public ?string $version, // Latest available version
public ?string $slug, // Plugin slug for WordPress
public ?string $plugin, // Plugin basename (slug/slug.php)
public ?string $downloadUrl, // Secure download URL
public ?\DateTimeImmutable $lastUpdated,// Date of the latest release
public ?string $tested, // Highest WordPress version tested
public ?string $requires, // Minimum WordPress version
public ?string $requiresPhp, // Minimum PHP version
public ?string $changelog, // Release notes
public ?string $packageHash, // SHA256 hash for integrity
public ?string $name, // Product name
public ?string $homepage, // Product homepage URL
public ?array $icons, // Plugin icons for WordPress admin
public ?array $sections, // Content sections for plugin info
) {}
public static function fromArray(array $data): self;
public function hasValidPackageHash(): bool; // Check package hash format
}
Usage:
$updateInfo = $client->checkForUpdates(
'ABCD-1234-EFGH-5678',
'example.com',
'my-plugin',
'1.0.0',
);
if ($updateInfo->updateAvailable) {
echo "New version available: " . $updateInfo->version;
echo "Download URL: " . $updateInfo->downloadUrl;
if ($updateInfo->hasValidPackageHash()) {
echo "Package hash: " . $updateInfo->packageHash;
}
}
Exception Hierarchy
All exceptions extend LicenseException and include an errorCode property for programmatic handling.
LicenseException (base)
├── LicenseNotFoundException // error: license_not_found
├── LicenseExpiredException // error: license_expired
├── LicenseRevokedException // error: license_revoked
├── LicenseInactiveException // error: license_inactive
├── LicenseInvalidException // error: license_invalid
├── DomainMismatchException // error: domain_mismatch
├── MaxActivationsReachedException // error: max_activations_reached
├── ActivationFailedException // error: activation_failed
├── ProductNotFoundException // error: product_not_found
├── RateLimitExceededException // error: rate_limit_exceeded (has retryAfter)
├── Security\SignatureException // error: signature_invalid
└── Security\IntegrityException // error: integrity_check_failed
Exception Properties
LicenseException (Base):
class LicenseException extends \RuntimeException
{
public readonly ?string $errorCode; // API error code for programmatic handling
}
RateLimitExceededException:
final class RateLimitExceededException extends LicenseException
{
public readonly ?int $retryAfter; // Seconds until rate limit resets
}
IntegrityException:
final class IntegrityException extends LicenseException
{
public readonly array $failures; // List of failed integrity checks
}
Catching Exceptions
use Magdev\WcLicensedProductClient\Exception\{
LicenseException,
LicenseNotFoundException,
LicenseExpiredException,
DomainMismatchException,
RateLimitExceededException,
};
use Magdev\WcLicensedProductClient\Security\SignatureException;
try {
$info = $client->validate($licenseKey, $domain);
} catch (SignatureException $e) {
// Response was tampered with or server secret mismatch
error_log("SECURITY: Response signature invalid");
die("License verification failed. Please contact support.");
} catch (LicenseNotFoundException $e) {
echo "License key not found.";
} catch (LicenseExpiredException $e) {
echo "Your license has expired. Please renew.";
} catch (DomainMismatchException $e) {
echo "This license is not valid for this domain.";
} catch (RateLimitExceededException $e) {
echo "Too many requests. Try again in {$e->retryAfter} seconds.";
} catch (LicenseException $e) {
// Catch-all for any license error
echo "License error: " . $e->getMessage();
error_log("License error [{$e->errorCode}]: " . $e->getMessage());
}
Security Classes
ResponseSignature
Verifies HMAC-SHA256 signatures on API responses to prevent tampering.
Key Derivation:
Each license key derives a unique signing key from the server secret:
// HKDF-like key derivation (internal implementation)
$prk = hash_hmac('sha256', $licenseKey, $serverSecret, true);
$derivedKey = hash_hmac('sha256', $prk . "\x01", $serverSecret);
Signature Format:
signature = HMAC-SHA256(
key = derived_key,
message = timestamp + ":" + canonical_json(response_body)
)
Where:
canonical_jsonsorts keys alphabetically- JSON uses
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODEflags - Result is hex-encoded (64 characters)
Response Headers:
| Header | Description | Example |
|---|---|---|
X-License-Signature |
HMAC-SHA256 signature (hex) | a1b2c3... (64 chars) |
X-License-Timestamp |
Unix timestamp when signed | 1706000000 |
Custom Timestamp Tolerance:
use Magdev\WcLicensedProductClient\Security\ResponseSignature;
// Default is 300 seconds (5 minutes)
$signature = new ResponseSignature($secretKey, timestampTolerance: 600); // 10 minutes
StringEncoder
Simple XOR-based string obfuscation for hiding API paths in source code.
WARNING: This is NOT encryption. It only prevents casual inspection.
use Magdev\WcLicensedProductClient\Security\StringEncoder;
$encoder = new StringEncoder();
// Encode strings for embedding in source code
$encoded = $encoder->encode('/wp-json/wc-licensed-product/v1');
// Result: "MR4xBi8rPDUcHhk2EQ0TICsvMBo9Mw0HJRE="
// Decode at runtime
$decoded = $encoder->decode('MR4xBi8rPDUcHhk2EQ0TICsvMBo9Mw0HJRE=');
// Result: "/wp-json/wc-licensed-product/v1"
// Generate multiple encoded constants
$constants = $encoder->generateEncodedConstants([
'API_PATH' => '/wp-json/wc-licensed-product/v1',
'VALIDATE' => 'validate',
'STATUS' => 'status',
]);
Custom Encoder Key:
$encoder = new StringEncoder('my_custom_encoder_key');
IntegrityChecker
Verifies that critical source files haven't been modified by comparing SHA256 hashes.
use Magdev\WcLicensedProductClient\Security\IntegrityChecker;
// Define expected hashes (generated during build)
$expectedHashes = [
'src/SecureLicenseClient.php' => 'abc123...',
'src/Security/ResponseSignature.php' => 'def456...',
];
$checker = new IntegrityChecker($expectedHashes, '/path/to/project');
// Verify all files
try {
$checker->verify();
echo "All files intact";
} catch (IntegrityException $e) {
echo "Tampered files: " . implode(', ', $e->failures);
}
// Or check silently
if (!$checker->isValid()) {
die("Critical files have been modified!");
}
Generating Hashes During Build:
$checker = new IntegrityChecker([], '/path/to/project');
$hashes = $checker->generateHashes([
'src/SecureLicenseClient.php',
'src/Security/ResponseSignature.php',
'src/Security/SignatureException.php',
]);
// Output as PHP code for embedding
echo $checker->generateHashesAsPhpCode([
'src/SecureLicenseClient.php',
'src/Security/ResponseSignature.php',
]);
Caching
Both clients support PSR-6 caching to reduce API calls. Cache keys use SHA256 hashes of license keys to avoid exposing sensitive data.
Cache Key Format:
wc_license_{operation}_{sha256(license_key)}[_{sha256(domain)}]
Example with Symfony Cache:
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
$cache = new FilesystemAdapter(
namespace: 'license_client',
defaultLifetime: 300,
directory: '/tmp/license-cache'
);
$client = new SecureLicenseClient(
httpClient: HttpClient::create(),
baseUrl: 'https://example.com',
serverSecret: 'your-secret',
cache: $cache,
cacheTtl: 600, // 10 minutes
);
Cache Behavior:
validate()andstatus()responses are cachedactivate()responses are NOT cached- After
activate(), related cache entries are automatically invalidated
Logging
Both clients support PSR-3 logging for debugging and monitoring.
Log Levels Used:
| Level | When Used |
|---|---|
debug |
Cache hits, signature verification success |
info |
API calls, successful operations |
warning |
API error responses, missing signatures |
error |
Request failures, network errors |
critical |
Integrity check failures |
Example with Monolog:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$logger = new Logger('license');
$logger->pushHandler(new StreamHandler('/var/log/license.log', Logger::DEBUG));
$client = new SecureLicenseClient(
httpClient: HttpClient::create(),
baseUrl: 'https://example.com',
serverSecret: 'your-secret',
logger: $logger,
);
Integration Guide
Basic Integration
The simplest integration for existing PHP projects:
<?php
// license-check.php
use Magdev\WcLicensedProductClient\SecureLicenseClient;
use Magdev\WcLicensedProductClient\Exception\LicenseException;
use Symfony\Component\HttpClient\HttpClient;
// Configuration (load from environment/config file)
$licenseKey = getenv('MY_PRODUCT_LICENSE_KEY');
$serverSecret = getenv('LICENSE_SERVER_SECRET');
$licenseServer = 'https://your-license-server.com';
$currentDomain = $_SERVER['HTTP_HOST'] ?? php_uname('n');
// Create client
$client = new SecureLicenseClient(
httpClient: HttpClient::create(),
baseUrl: $licenseServer,
serverSecret: $serverSecret,
);
// Validate license
try {
$info = $client->validate($licenseKey, $currentDomain);
// Define constant for use throughout application
define('MY_PRODUCT_LICENSED', true);
define('MY_PRODUCT_ID', $info->productId);
} catch (LicenseException $e) {
define('MY_PRODUCT_LICENSED', false);
define('MY_PRODUCT_ID', null);
// Log the error but don't expose details to users
error_log("License validation failed: " . $e->getMessage());
}
WordPress Plugin Integration
<?php
/**
* Plugin Name: My Licensed Plugin
*/
namespace MyPlugin;
use Magdev\WcLicensedProductClient\SecureLicenseClient;
use Magdev\WcLicensedProductClient\Exception\LicenseException;
use Symfony\Component\HttpClient\HttpClient;
class LicenseManager
{
private const LICENSE_OPTION = 'my_plugin_license_key';
private const SERVER_URL = 'https://your-license-server.com';
private const SERVER_SECRET = 'your-server-secret'; // Or load from wp-config.php
private ?SecureLicenseClient $client = null;
public function __construct()
{
$this->client = new SecureLicenseClient(
httpClient: HttpClient::create(),
baseUrl: self::SERVER_URL,
serverSecret: self::SERVER_SECRET,
);
}
public function isLicenseValid(): bool
{
$licenseKey = get_option(self::LICENSE_OPTION);
if (empty($licenseKey)) {
return false;
}
// Use transient for caching
$cacheKey = 'my_plugin_license_valid_' . md5($licenseKey);
$cached = get_transient($cacheKey);
if ($cached !== false) {
return $cached === 'valid';
}
try {
$domain = wp_parse_url(home_url(), PHP_URL_HOST);
$this->client->validate($licenseKey, $domain);
set_transient($cacheKey, 'valid', HOUR_IN_SECONDS);
return true;
} catch (LicenseException $e) {
set_transient($cacheKey, 'invalid', 5 * MINUTE_IN_SECONDS);
return false;
}
}
public function activateLicense(string $licenseKey): array
{
try {
$domain = wp_parse_url(home_url(), PHP_URL_HOST);
$result = $this->client->activate($licenseKey, $domain);
if ($result->success) {
update_option(self::LICENSE_OPTION, $licenseKey);
delete_transient('my_plugin_license_valid_' . md5($licenseKey));
}
return [
'success' => $result->success,
'message' => $result->message,
];
} catch (LicenseException $e) {
return [
'success' => false,
'message' => $e->getMessage(),
];
}
}
}
Laravel Integration
Service Provider:
<?php
// app/Providers/LicenseServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Magdev\WcLicensedProductClient\LicenseClientInterface;
use Magdev\WcLicensedProductClient\SecureLicenseClient;
use Symfony\Component\HttpClient\HttpClient;
use Illuminate\Log\Logger;
use Illuminate\Cache\CacheManager;
class LicenseServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(LicenseClientInterface::class, function ($app) {
return new SecureLicenseClient(
httpClient: HttpClient::create(),
baseUrl: config('services.license.url'),
serverSecret: config('services.license.secret'),
logger: $app->make(Logger::class),
cache: $app->make(CacheManager::class)->store('file')->getStore(),
);
});
}
}
Configuration (config/services.php):
return [
// ...
'license' => [
'url' => env('LICENSE_SERVER_URL'),
'secret' => env('LICENSE_SERVER_SECRET'),
],
];
Usage in Controller:
<?php
namespace App\Http\Controllers;
use Magdev\WcLicensedProductClient\LicenseClientInterface;
use Magdev\WcLicensedProductClient\Exception\LicenseException;
class LicenseController extends Controller
{
public function __construct(
private readonly LicenseClientInterface $licenseClient,
) {}
public function validate(Request $request)
{
try {
$info = $this->licenseClient->validate(
$request->input('license_key'),
$request->getHost(),
);
return response()->json([
'valid' => true,
'product_id' => $info->productId,
'expires_at' => $info->expiresAt?->format('Y-m-d'),
]);
} catch (LicenseException $e) {
return response()->json([
'valid' => false,
'error' => $e->errorCode,
'message' => $e->getMessage(),
], 403);
}
}
}
Symfony Integration
Service Configuration (config/services.yaml):
services:
Magdev\WcLicensedProductClient\LicenseClientInterface:
class: Magdev\WcLicensedProductClient\SecureLicenseClient
arguments:
$httpClient: '@Symfony\Contracts\HttpClient\HttpClientInterface'
$baseUrl: '%env(LICENSE_SERVER_URL)%'
$serverSecret: '%env(LICENSE_SERVER_SECRET)%'
$logger: '@logger'
$cache: '@cache.app'
Best Practices
1. Use the Secure Client in Production
Always use SecureLicenseClient in production to verify response signatures:
// Development
$client = new LicenseClient(...);
// Production
$client = new SecureLicenseClient(..., serverSecret: $secret);
2. Handle All Exception Types
Don't catch just the base exception. Handle specific cases:
try {
$client->validate($key, $domain);
} catch (SignatureException $e) {
// Critical: possible tampering
notifyAdmin("Signature verification failed!");
die("Security error");
} catch (RateLimitExceededException $e) {
// Retry later
sleep($e->retryAfter ?? 60);
retry();
} catch (LicenseExpiredException $e) {
// Prompt renewal
showRenewalForm();
} catch (LicenseException $e) {
// Generic handling
showError($e->getMessage());
}
3. Cache Validation Results
Use caching to minimize API calls:
$cache = new FilesystemAdapter('licenses', 300);
$client = new SecureLicenseClient(..., cache: $cache, cacheTtl: 300);
4. Store Secrets Securely
Never hardcode secrets in source code:
// Bad
$client = new SecureLicenseClient(..., serverSecret: 'abc123');
// Good
$client = new SecureLicenseClient(..., serverSecret: getenv('LICENSE_SECRET'));
5. Validate on Application Startup
Check license validity early, not on every request:
// bootstrap.php
$isLicensed = checkLicense();
define('APP_LICENSED', $isLicensed);
// Later in code
if (APP_LICENSED) {
showPremiumFeatures();
}
6. Log License Events
Enable logging for debugging and audit trails:
$client = new SecureLicenseClient(
...,
logger: new Logger('license'),
);
7. Implement Graceful Degradation
Don't crash the entire application on license failure:
try {
$info = $client->validate($key, $domain);
enablePremiumFeatures($info);
} catch (LicenseException $e) {
// Fall back to free tier
enableFreeFeatures();
logLicenseIssue($e);
}
API Reference
API Endpoints
The client communicates with these REST API endpoints:
| Endpoint | Method | Description |
|---|---|---|
/wp-json/wc-licensed-product/v1/validate |
POST | Validate license for domain |
/wp-json/wc-licensed-product/v1/status |
POST | Get license status details |
/wp-json/wc-licensed-product/v1/activate |
POST | Activate license on domain |
/wp-json/wc-licensed-product/v1/update-check |
POST | Check for plugin updates |
Request Format
All requests use JSON bodies:
{
"license_key": "ABCD-1234-EFGH-5678",
"domain": "example.com"
}
Response Format
Successful Validation:
{
"valid": true,
"license": {
"product_id": 123,
"expires_at": "2027-01-21",
"version_id": null
}
}
Error Response:
{
"valid": false,
"error": "license_expired",
"message": "This license has expired."
}
Error Codes
| Error Code | Exception Class | Description |
|---|---|---|
license_not_found |
LicenseNotFoundException |
License key doesn't exist |
license_expired |
LicenseExpiredException |
License past expiration date |
license_revoked |
LicenseRevokedException |
License manually revoked |
license_inactive |
LicenseInactiveException |
License not activated |
license_invalid |
LicenseInvalidException |
License invalid for operation |
domain_mismatch |
DomainMismatchException |
Domain not authorized |
max_activations_reached |
MaxActivationsReachedException |
Too many activations |
activation_failed |
ActivationFailedException |
Server error during activation |
product_not_found |
ProductNotFoundException |
Licensed product doesn't exist |
rate_limit_exceeded |
RateLimitExceededException |
Too many requests |
signature_invalid |
SignatureException |
Response signature invalid |
integrity_check_failed |
IntegrityException |
Source files modified |
Troubleshooting
"Response is not signed by the server"
- Server not configured with
WC_LICENSE_SERVER_SECRET - Using
SecureLicenseClientwith non-signing server - Solution: Configure server secret or use
LicenseClient
"Response signature verification failed"
- Different secrets on server/client
- Clock skew > 5 minutes between server and client
- Response modified by proxy/cache
- Solution: Verify secrets match, sync clocks, disable response caching
Connection Timeouts
- Network issues between client and license server
- Solution: Configure HTTP client timeout
$httpClient = HttpClient::create([
'timeout' => 30,
'max_duration' => 60,
]);
Rate Limiting
- Too many requests from same IP
- Solution: Implement caching, handle
RateLimitExceededException
catch (RateLimitExceededException $e) {
$waitSeconds = $e->retryAfter ?? 60;
// Implement exponential backoff
}