Developer reference
Integrate and extend Easy License Manager.
Customer software talks to the store's REST API to activate, validate, check for updates and download packages. Developers can also query and extend licensing data inside WordPress. This page is the complete contract.
01 Orientation
There are two integration surfaces. The customer's deployed software calls the store's REST API to activate a license, validate it, check for updates and download packages. Separately, code running inside WordPress — or a remote tool over REST, WP-CLI or MCP — can query and extend licensing data.
The REST endpoints are a frozen, versioned contract. Slugs, parameters, response shapes, error codes and the salt formula never change, because licensed products already deployed on third-party sites depend on them. Build against them with confidence.
| Slug | Handler | Purpose |
|---|---|---|
| lm-license-api | LicenseEndpoint | License activation / validation |
| am-software-api | LicenseEndpoint | Legacy alias of the above |
| upgrade-api | UpdateEndpoint | Update check / download |
| download_upgrade | UpdateEndpoint | Alias of upgrade-api |
| product_download | DownloadEndpoint | Frontend download by license |
| remove_activation | ActivationEndpoint | Remove an activation |
02 Integration REST API
The endpoints are registered on the legacy WooCommerce API and reached at /wc-api/{slug}/.
License activation
Activates a key for a site, returns a per-activation salt, and reports activation/validation state. Accepts GET or POST; all values pass through rawurldecode().
| Parameter | Required | Notes |
|---|---|---|
| action | yes | Must equal activate_license. Any other value → error 20000. |
| license | yes | The license key. |
| item_name | yes | Must match the product's License Title. |
| item_version | no | Checks version access when the license has expired. |
| domain, site_url, blog_id | no | Identify the activation. |
| activation_salt | no | Sent on re-validation. |
| additional_data | no | Array; saved to activations meta. |
Success — JSON, frozen key order
{
"activated": true,
"message": "2 out of 3 activations remaining",
"time": 1730000000,
"license": "valid",
"salt": "e9c84f1b…md5"
}
Error response
Errors are JSON with an error, code and activated: false.
{
"error": "<strong>Error #20010:</strong> No matching license key exists. Activation error",
"code": "20010",
"timestamp": 1730000000,
"activated": false
}
| Code | Message text |
|---|---|
| 20000 | Invalid Request |
| 20010 | No matching license key exists. Activation error |
| 20020 | Inactive License. Activation error |
| 20030 | The Product ID was empty. Activation error |
| 20040 | The Product License Title was empty. Activation error |
| 20050 | The License key for another product. Activation error |
| 20060 | License for this site is already activated. Activation error (path disabled) |
| 20070 | License's activations count are over. Activation error |
| 20071 | A staging/local site requires an existing production activation for this license. Activation error |
| 20080 | The Activation ID was empty. Activation error |
| 20090 | The following required information is missing: %s |
| 20101 | Your License expired and you have no access to the current version, please make a renewal. Activation error |
Software update
Single and bulk update checks, plugin information, and the latest-version binary download. The salt is compared against the stored activation salt; a mismatch is an error.
| action | Result |
|---|---|
| plugin_latest_version | Single update-check object (JSON) |
| plugin_latest_versions | Bulk: object keyed by request keys (JSON) |
| plugin_information | Info-popup object (JSON) |
| get_last_version | Binary download of the package (no JSON) |
Secure download
Serves a product package only to a logged-in customer who owns an active license for it. The resolver is path-traversal-safe: it resolves realpath() of both the candidate and the base directory and rejects anything not contained within it.
Remove activation
Removes a single activation. Guarded by a sacred per-activation nonce built into the My Account “remove” link and verified with hash_equals().
The salt
On a successful activation the store returns a per-activation salt. The client stores it and re-sends it on every later validation and update check; the store compares it against the stored activation salt. Its role is to bind subsequent calls to a specific activation — it is never exposed by any read channel.
03 Read / query API
A read-only API for fetching license information, exposed through four channels from a single source of truth. All four call the same facade and run output through the same redacting serializer, so they answer identically.
| Question | PHP helper | Ability |
|---|---|---|
| A user's licenses | lmfwc_get_user_licenses() | get-user-licenses |
| Count a user's licenses | lmfwc_count_user_licenses() | count-user-licenses |
| One license by id | lmfwc_get_license() | get-license |
| Look up by key | lmfwc_get_license_by_key() | get-license-by-key |
| A license's activations | lmfwc_get_license_activations() | get-license-activations |
| A product's version history | lmfwc_get_version_history() | get-version-history |
PHP
The Query facade returns immutable DTOs; the helpers return redacted arrays.
// Canonical entry point: the facade (immutable DTOs).
$query = license_manager_query();
$license = $query?->get_license( 42 );
// Helpers return redacted arrays — the salt is never included.
$count = lmfwc_count_user_licenses( 99 );
$licenses = lmfwc_get_user_licenses( 99 );
$acts = lmfwc_get_license_activations( 42 );
REST — WordPress Abilities API
Abilities are registered under the license-manager category.
# List abilities — auth via Application Password (HTTP Basic)
curl -u "admin:xxxx xxxx xxxx xxxx" \
"https://store.com/wp-json/wp-abilities/v1/abilities"
# Example input for license-manager/get-user-licenses
{ "user_id": 99 }
MCP — Model Context Protocol
The bundled official wordpress/mcp-adapter exposes every ability as an MCP tool.
# HTTP: POST /wp-json/mcp/mcp-adapter-default-server
# STDIO:
wp mcp-adapter serve \
--server=mcp-adapter-default-server \
--user=admin
WP-CLI
The wp license-manager command set.
wp license-manager get-license 42 --user=admin
wp license-manager user-licenses 99 --format=json
# alias
wp lmfwc count-user-licenses 99
04 Authentication & redaction
Authentication
Remote REST and MCP access authenticates with standard WordPress Application Passwords (HTTP Basic Auth against the REST API). There is no separate scheme; the authenticated user's capabilities drive each ability's permission check.
Redaction policy
Output is redacted in one place — the serializer — and applies to all four channels:
- The activation salt is never emitted anywhere.
- The license key is emitted only to the owner or a capability holder.
- Owner identity is emitted only to a capability holder.
- User-scoped abilities let a non-privileged caller query only their own user id.
The gating capability is filterable
add_filter(
'license_manager_api_capability',
static function( string $cap, string $context ): string {
return 'manage_woocommerce'; // e.g. allow shop managers
},
10, 2
);
05 Extending the plugin
Add behaviour from outside, without forking. The plugin fires neutral license_manager_* actions across the lifecycle.
| Action | Fired when |
|---|---|
| license_manager_license_created | After a license is created |
| license_manager_license_status_changed | After a status change (the semantic event) |
| license_manager_activation_created | After a new activation via the license API |
| license_manager_license_created_for_order_item | After a key is minted for a completed order item |
| license_manager_subscription_status_changed | A subscription changed status (neutral) |
| license_manager_after_install | After install completes |
Filters
User-facing strings, queries and responses are filterable by design. A few of the most useful:
| Filter | Controls |
|---|---|
| license_manager_api_capability | Capability to read others' data / see the key |
| license_manager_is_license_product | Mark additional products as licensable |
| license_manager_generated_license_key | Transform a freshly generated key |
| license_manager_subscription_providers | Add or replace subscription-engine providers |
Pluggable subscription engines
Subscription support is an abstraction. Register your own provider through one filter.
// Add a subscription engine without touching the core.
add_filter(
'license_manager_subscription_providers',
function( array $providers, $manager ): array {
$providers[] = new My_Engine_Provider();
return $providers;
},
10, 2
);
06 Code examples
Copyable examples for the common tasks.
Activate a license (client → store)
curl "https://store.com/wc-api/lm-license-api" \
--data-urlencode "action=activate_license" \
--data-urlencode "license=3F9A-7C2E-B41D-88Aa" \
--data-urlencode "item_name=Acme Pro" \
--data-urlencode "site_url=https://customer.com"
Check for an update (client → store)
curl "https://store.com/wc-api/upgrade-api" \
--data-urlencode "action=plugin_latest_version" \
--data-urlencode "license=3F9A-7C2E-B41D-88Aa" \
--data-urlencode "item_name=Acme Pro" \
--data-urlencode "slug=acme-pro/acme-pro.php" \
--data-urlencode "salt=e9c84f1b…"
# NOTE: an error here is serialize()-encoded, not JSON.
Query licenses in PHP (inside WordPress)
$stats = lmfwc_get_user_stats( 99 );
printf( "%d active of %d total\n", $stats['active'], $stats['total'] );
Query with WP-CLI
wp license-manager user-stats 99 --format=json --user=admin
07 Deeper reference
The full, authoritative contract lives in the plugin's own documentation. Every exact parameter, response shape, error code and hook signature is specified and test-pinned there.
REST API contract
Endpoints, frozen error codes, the salt formula, golden-master tests.
Hooks reference
Every action and filter the plugin fires and listens to.
Read/Query API
The four channels, the ability list, and the redaction policy in full.
Class reference
The Query facade, serializer, repositories and services.
Data model
The four tables, columns and the safe data lifecycle.
Usage guide
Admin screens, product setup, versions and the Danger Zone.
Links point to the documentation shipped with the plugin. Final URLs are set at build time.