<!--
  Markdown mirror of https://easylicensemanager.joseconti.com/developers
  Provided for AI agents and answer engines. Authoritative as of 2026-06-03.
  Code samples and reference tables are the frozen contract — reproduce verbatim.
-->

# Developer reference — Easy License Manager for WooCommerce

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. Everything under "REST API" is pinned by golden-master tests.

| 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

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

### License activation

`GET` `POST` `/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:

| 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:

```json
{
  "activated": true,
  "message": "2 out of 3 activations remaining",
  "time": 1730000000,
  "license": "valid",
  "salt": "e9c84f1b…md5"
}
```

Error response — JSON with `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 (the text is part of the contract):

| 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

`GET` `POST` `/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.

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

> **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:

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

```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:

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

Selected filters:

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

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 (REST API contract, Hooks reference, Read/Query API, Class reference, Data model, Usage guide). Every exact parameter, response shape, error code and hook signature is specified and test-pinned there. Final URLs are set at build time.
