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.

Frozen REST contract WordPress 6.9+ PHP 8.1+ Application Passwords

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.

Frozen contract. Everything under “REST API” is pinned by golden-master tests.
SlugHandlerPurpose
lm-license-apiLicenseEndpointLicense activation / validation
am-software-apiLicenseEndpointLegacy alias of the above
upgrade-apiUpdateEndpointUpdate check / download
download_upgradeUpdateEndpointAlias of upgrade-api
product_downloadDownloadEndpointFrontend download by license
remove_activationActivationEndpointRemove an activation

02 Integration REST API

The endpoints are registered on the legacy WooCommerce API and reached at /wc-api/{slug}/.

License activation

GETPOST /wc-api/lm-license-api alias · am-software-api

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().

Request parameters
ParameterRequiredNotes
actionyesMust equal activate_license. Any other value → error 20000.
licenseyesThe license key.
item_nameyesMust match the product's License Title.
item_versionnoChecks version access when the license has expired.
domain, site_url, blog_idnoIdentify the activation.
activation_saltnoSent on re-validation.
additional_datanoArray; saved to activations meta.

Success — JSON, frozen key order

json200 OK
{
  "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.

json
{
  "error": "<strong>Error #20010:</strong> No matching license key exists. Activation error",
  "code": "20010",
  "timestamp": 1730000000,
  "activated": false
}
Error codes — frozen (text is part of the contract)
CodeMessage text
20000Invalid Request
20010No matching license key exists. Activation error
20020Inactive License. Activation error
20030The Product ID was empty. Activation error
20040The Product License Title was empty. Activation error
20050The License key for another product. Activation error
20060License for this site is already activated. Activation error (path disabled)
20070License's activations count are over. Activation error
20071A staging/local site requires an existing production activation for this license. Activation error
20080The Activation ID was empty. Activation error
20090The following required information is missing: %s
20101Your License expired and you have no access to the current version, please make a renewal. Activation error

Software update

GETPOST /wc-api/upgrade-api alias · download_upgrade

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.

Actions
actionResult
plugin_latest_versionSingle update-check object (JSON)
plugin_latest_versionsBulk: object keyed by request keys (JSON)
plugin_informationInfo-popup object (JSON)
get_last_versionBinary download of the package (no JSON)
Errors are serialize()-encoded, not JSON. Clients must unserialize() them.

Secure download

GET/wc-api/product_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

GET/wc-api/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.

REST and MCP channels require WordPress 6.9+ (the Abilities API ships in core from 6.9). The PHP facade and WP-CLI work on any supported version.
What you can ask
QuestionPHP helperAbility
A user's licenseslmfwc_get_user_licenses()get-user-licenses
Count a user's licenseslmfwc_count_user_licenses()count-user-licenses
One license by idlmfwc_get_license()get-license
Look up by keylmfwc_get_license_by_key()get-license-by-key
A license's activationslmfwc_get_license_activations()get-license-activations
A product's version historylmfwc_get_version_history()get-version-history

PHP

The Query facade returns immutable DTOs; the helpers return redacted arrays.

php
// 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.

bash
# 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.

bash
# 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.

bash
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

php
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.

A selection of lifecycle actions
ActionFired when
license_manager_license_createdAfter a license is created
license_manager_license_status_changedAfter a status change (the semantic event)
license_manager_activation_createdAfter a new activation via the license API
license_manager_license_created_for_order_itemAfter a key is minted for a completed order item
license_manager_subscription_status_changedA subscription changed status (neutral)
license_manager_after_installAfter install completes

Filters

User-facing strings, queries and responses are filterable by design. A few of the most useful:

Selected filters
FilterControls
license_manager_api_capabilityCapability to read others' data / see the key
license_manager_is_license_productMark additional products as licensable
license_manager_generated_license_keyTransform a freshly generated key
license_manager_subscription_providersAdd or replace subscription-engine providers

Pluggable subscription engines

Subscription support is an abstraction. Register your own provider through one filter.

Within the contract. Filters touching the REST flow operate inside the frozen contract.
php
// 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)

bash
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)

bash
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)

php
$stats = lmfwc_get_user_stats( 99 );
printf( "%d active of %d total\n", $stats['active'], $stats['total'] );

Query with WP-CLI

bash
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.

Links point to the documentation shipped with the plugin. Final URLs are set at build time.