Licensing

Licensing and License Key Systems

⏱ 16 min

Once your product has a price, something has to enforce who’s actually paid for it. That’s a license key system: a way to issue a key at purchase time, let the plugin activate against that key, tie the activation to a specific site, and check it periodically so updates and support stay tied to a valid license. You can build this yourself, and plenty of developers do, but two real, well-established services already solve most of it. This lesson covers both neutrally, plus what a license key system needs to do functionally regardless of which path you take.

What you'll learn in this lesson
What a license key system actually needs to do
Activate, deactivate, tie to a site URL, and check on update, independent of any vendor.
Easy Digital Downloads with Software Licensing
What the extension actually provides, as a real, existing option.
Freemius
What it actually provides, as a real, existing option, and how it differs in approach.
A basic license-check pattern in PHP
The shape of the client-side code your plugin runs against either service, or your own.
Prerequisites

This lesson assumes you’ve settled on a pricing model from Lesson 2. It doesn’t require you to have picked EDD or Freemius yet, that’s exactly the decision this lesson is for.

Step 1: What a license key system has to do, regardless of vendor

Strip away any particular product and a license key system is really four functions:

The functional core of any license system
Issue a key at purchase
Tied to a customer, a product, and usually a number of allowed site activations.
Activate against a specific site URL
So the same key can't be silently reused across unlimited sites without your knowledge.
Deactivate
So a customer moving a license from a staging site to production, or between two of their own sites, isn't locked out.
Check on update
So automatic plugin updates (and, if you sell one, access to a paid add-on's changelog) are gated to a currently valid license.

Everything else, renewal reminders, upgrade paths, refund handling, is built on top of these four functions. Whether you build them yourself against a custom REST endpoint, or use a service that already provides them, the plugin-side code your customers run looks broadly similar.

Step 2: Two real, named options

Easy Digital Downloads (Software Licensing) versus Freemius
Easy Digital Downloads + Software LicensingFreemius
What it isA WordPress-native e-commerce plugin (EDD) plus an official extension purpose-built for license key generation, activation, deactivation, version checks, and update deliveryA hosted SaaS platform acting as merchant of record, handling checkout, subscriptions, taxes and compliance, licensing, and a customer self-service portal
Where it runsOn your own WordPress site, you own the store and the dataHosted by Freemius, your plugin integrates with their SDK and dashboard
What it handles for youLicense key generation and validation, activation limits per license, renewal reminders and discounts, update delivery through the standard WordPress updater UI, upgrade/renewal reportingCheckout and subscriptions, global tax and compliance handling, licensing with activations and renewals, a customer portal for licenses and billing, affiliate management, product analytics
Good fit forDevelopers who want to run their own store on their own site and already use, or are comfortable running, WordPress-based e-commerceDevelopers who want to offload payments, tax compliance, and the checkout/billing surface entirely to avoid building or maintaining it themselves
Neither is an endorsement, both are real and worth evaluating

This course names EDD and Freemius because they’re real, well-known, and specifically built for this problem in the WordPress ecosystem, not because one is objectively better. Evaluate both against your own pricing model, your comfort running your own store versus using a hosted merchant of record, and your actual support and tax situation.

Step 3: The shape of a license check, in plugin code

Whichever service you use (or if you build your own), the plugin-side pattern is similar: store the license key, call a remote endpoint to validate it, cache the result so you’re not checking on every page load, and gate update access on a valid response.

// File: includes/class-license-check.php
class My_Plugin_License {

	private $api_url = 'https://my-site.com/wp-json/my-plugin/v1/license';

	public function is_valid() {
		$cached = get_transient( 'my_plugin_license_status' );
		if ( false !== $cached ) {
			return 'valid' === $cached;
		}

		$license_key = get_option( 'my_plugin_license_key' );
		if ( empty( $license_key ) ) {
			return false;
		}

		$response = wp_remote_post( $this->api_url, array(
			'timeout' => 15,
			'body'    => array(
				'license'    => $license_key,
				'site_url'   => home_url(),
				'action'     => 'check',
			),
		) );

		if ( is_wp_error( $response ) ) {
			// Network failure: don't lock the customer out, re-check soon instead.
			set_transient( 'my_plugin_license_status', 'valid', HOUR_IN_SECONDS );
			return true;
		}

		$body   = json_decode( wp_remote_retrieve_body( $response ), true );
		$status = isset( $body['status'] ) ? $body['status'] : 'invalid';

		set_transient( 'my_plugin_license_status', $status, 12 * HOUR_IN_SECONDS );
		return 'valid' === $status;
	}
}

A few decisions in that snippet are deliberate. The transient caches the result so a license isn’t re-validated on every single page load, which would add latency and hit your license server unnecessarily. The home_url() value ties the check to a specific site, matching the “activate against a site URL” requirement from Step 1. And a network failure fails open (treats the license as valid for the caching window) rather than locking a paying customer out of their own site because your license server had a bad minute, a real and common failure mode worth deciding on deliberately rather than by accident.

Failing closed on a network error punishes paying customers for your infrastructure

If your license server is briefly unreachable and your check fails closed (treats the customer as unlicensed), you disable a paying customer’s plugin because of your own downtime, not theirs. Decide this behavior on purpose. Most real systems, including EDD and Freemius, fail open for a grace period rather than immediately locking out an active customer.

Test it

Point the snippet above at a mock endpoint (or a real one on a staging license server) and confirm all three paths behave as intended:

# Valid license
curl -s -X POST https://my-site.com/wp-json/my-plugin/v1/license \
  -d "license=TEST-VALID&site_url=http://example.test&action=check"

# Invalid license
curl -s -X POST https://my-site.com/wp-json/my-plugin/v1/license \
  -d "license=TEST-INVALID&site_url=http://example.test&action=check"

Then simulate a network failure (block the request, or point at a nonexistent host) and confirm the plugin still treats the site as licensed for the length of the cached grace window, rather than immediately disabling functionality.

Recap

A license key system, whether you build it or use EDD’s Software Licensing extension or Freemius, needs to activate, deactivate, tie to a site URL, and check on update. EDD keeps the store on your own WordPress site and gives you direct ownership of licensing data; Freemius acts as a hosted merchant of record and takes on payments, tax compliance, and the checkout surface entirely. Whichever you choose, decide deliberately how your plugin behaves when the license check itself fails, since failing closed on a network error punishes the customer for your infrastructure, not theirs.

Resources & further reading

← Pricing Models for AI Features: Usage, Credits, BYO-Key, or Managed GPL Compliance for Commercial WordPress Plugins →