Milo License Server

Sell software licenses tied to your subscriptions: key generation, site activations, plugin update delivery, GitHub release sync, and evergreen download URLs, all from your own store.

Installation

  1. Install and activate the free Milo Subscriptions plugin, or WooCommerce (the license server requires at least one of the two).
  2. Upload and activate Milo License Server (Plugins → Add New → Upload).
  3. Enter your license key under Subscriptions → Licenses to enable updates.

Activation creates a dedicated licenses table plus a telemetry table for update pings and downloads. The admin lives under Subscriptions → Licenses with four screens: All Licenses, Products, Insights, and Settings.

Configuration

Under Subscriptions → Licenses → Settings:

Setting Description
Grace period (days) Days a customer keeps updates after expiry (default 14).
Auto-create licenses On/off switch for automatic issuance.
GitHub webhook secret Shared secret used to verify release webhooks.
GitHub token Token used to download release assets (needed for private repositories).
GitHub organization Optional owner allowlist; releases from other owners are ignored.

Selling licensed products (WooCommerce)

Every WooCommerce product gets a License & Updates tab with these fields: Licensed product (on/off), Bundle license (one key valid for all paid products, current and future), Product slug, Site limit (0 = unlimited), Current version, Changelog, Tested up to (WP), Requires WP, and Requires PHP. Upload the plugin zip as a downloadable file on the product, or point it at the evergreen URL (below).

When an order reaches processing or completed, a key is generated per licensed line item (format MILO-XXXX-XXXX-XXXX-XXXX), valid one year. Keys are shown on the order confirmation page and in order emails, and customers find them any time in a My Account → Licenses tab (with a Copy button); the Downloads page also gains a Version column.

The subscription lifecycle drives the license: a renewal payment extends it one year, cancellation or expiry expires it (grace period applies), and a full refund revokes it immediately. Renewal orders never generate duplicate keys. WooCommerce Subscriptions hooks are also supported.

Standalone plans (no WooCommerce)

The license server core runs without WooCommerce. A Milo Subscriptions standalone plan with licensing enabled (product slug, site limit, optional bundle flag in its license configuration) issues a key when the subscription becomes active or trialing, extends it to the new period end on each renewal, and expires it with grace on cancellation. Keys appear in the standalone account area.

Licenses and activations

Subscriptions → Licenses → All Licenses lists every key with search, a status filter, and a Source column showing how it was issued (WooCommerce order, standalone subscription, or manual). You can create a key manually (product slug, customer email, site limit, expiry; defaults to one year), revoke one (immediate, no grace), or reactivate a revoked or expired one.

Activation notes:

  • Customers activate a key from their site; each key allows up to its site limit of production sites.
  • Staging and development domains (.local, .test, .dev, localhost, ngrok, DDEV, Flywheel, and similar patterns) never count against the limit.
  • Expired keys keep validating during the grace period with a “please renew” status, then stop.
  • A license with the special “internal” status bypasses expiry and site limits entirely; it is meant for the store owner’s own sites and can only be disabled by revoking it.
  • Bundle licenses validate against any paid product; free products are excluded from bundles.

Serving updates to customer plugins

Client plugins talk to public REST endpoints under milo-license/v1: activate, deactivate, validate, update-check, and download. The update check validates the key, compares versions, and returns the new version, changelog, tested/requires metadata, and a license-guarded download URL, so updates appear on the customer’s native WordPress Updates screen. Downloads are streamed after a fresh license check on every request.

Free products (marked “free” in the Products screen with a public download URL) are served through an anonymous free-update-check endpoint, no key required; their downloads pass through a tracking wrapper so they are counted in Insights.

The licence client for your plugins

The licence field, the “Enter license” link on the Plugins screen, the unlicensed notice and the “View details” popup all have to live inside the plugin you are selling. That is not a limitation to work around: License Server runs on your shop, and your customer’s site has none of it. The only thing of yours on that site is the plugin itself, so that is where the client has to travel.

You do not have to write one. A ready-made client ships in this plugin at client/class-license-client.php. Copy it into your plugin, require it, and construct it once:

require_once __DIR__ . '/includes/class-license-client.php';

new Milo_LS_Client( array(
    'product_slug' => 'your-plugin',      // must match the product slug on your shop
    'product_name' => 'Your Plugin',
    'plugin_file'  => __FILE__,           // the main plugin file
    'server_url'   => 'https://your-shop.com',
) );

That is the whole integration. It gives your customers a licence screen under Settings, an “Enter license” link next to Activate and Deactivate, a notice while the plugin is unlicensed, updates on their normal Updates screen, and release notes in the “View details” popup.

Rename the class to something of your own if you prefer. Keep it unique: two plugins defining the same class name on one site would collide, which is why the file returns early if the name is already taken.

Two details worth knowing. The client answers WordPress’s plugin_information request itself, always, once the slug matches: handing that request back to WordPress sends it to wordpress.org, which has never heard of a privately sold plugin, and the customer sees “Plugin not found.” instead of your release notes. And licence validity is cached for a day, because checking on every page load would make your customers’ admin depend on your server being reachable.

Prefer to build your own? Everything above is only the five public endpoints. activate and deactivate take license_key and site_url and return success with a message; validate returns valid, status and the licence; update-check additionally takes product_slug and version, and returns update_available, version, download_url, the tested and requires metadata, and the release notes under sections.changelog.

GitHub release sync

Ship a release on GitHub and the server ingests it automatically:

  1. Set the GitHub webhook secret (and a token for private repositories) in Subscriptions → Licenses → Settings.
  2. Add one webhook, repository or organization-wide, pointing at /wp-json/milo-license-server/v1/github-webhook with content type application/json, your secret, and the Releases event.

On every published release, the zip asset is downloaded, stored as the product package, the version (from the tag) is propagated to all licenses, and the release notes become the changelog. The product slug defaults to the repository name (filterable). If CI is still uploading the asset, processing retries with backoff via Action Scheduler. A capped log of the last 50 syncs is kept.

Evergreen package URLs

Every registered product gets a stable, token-guarded URL of the form /milo-package/{slug}/{token}/{slug}.zip that always serves the latest pushed package. Copy it from Subscriptions → Licenses → Products, point your WooCommerce downloadable file at it once, and stop re-uploading zips on every release. The /milo-package/ prefix is auto-approved for WooCommerce’s Approved Download Directories, and these files are always served via redirect so they work whatever download method the store uses. The token is generated once per product and never changes across pushes.

The shortcode and block

Show a registered product’s latest version and changelog on any page, always in sync with the update server. The “License Server Product Info” block is the editor equivalent with the same attributes:

Attribute Description
slug The registered product slug (required).
show version, changelog, or both (default both).
version_label Custom label for the version line (default “Latest version”).
updated_label Custom label for the release date (default “Released”).

Insights

Subscriptions → Licenses → Insights shows active installs (from update-check heartbeats), total downloads, and the version distribution per product, over a selectable range.

Troubleshooting

  • No key was generated for an order: check the product’s License & Updates tab has Licensed product enabled and a product slug (unless it is a bundle), and that the order reached processing or completed.
  • A customer cannot activate: the key may be at its site limit (they can deactivate an old site from their plugin’s license screen), expired past grace, or revoked.
  • Updates not showing on customer sites: confirm the product is registered under Licenses → Products with a version higher than the installed one, and that a package zip has been pushed.
  • GitHub webhook returns “not configured” or a signature error: set the webhook secret in Settings and use the exact same value on GitHub, with content type application/json.
  • A GitHub release did not sync: only the “published” action is processed; check the sync needs a zip asset on the release and see the sync log for “waiting for asset” or failure entries.

Billing modes

Works in both WooCommerce mode and standalone (Stripe-driven) mode.

Did this page miss something?

Tell us and we rewrite it, usually the same day.

Send a note