Licensing Integration Guide

This guide is for you if you sell a WordPress plugin, theme, or similar product through Avenia Digital Products and need customers to activate a license key on their own site. It walks through store setup, adding a license field to your product’s settings screen, and calling the license API.

If you only sell files (ebooks, templates, audio) and do not need activation, you can skip this guide. Store owners who enable keys without writing code should read License Keys instead.

For Dashboard auto-updates of the plugin/theme you sell, also read WordPress Plugin and Theme Updates Guide and copy the updater php code sample.

How The Pieces Fit Together

There are always two WordPress sites in a normal sale:

  • Your store, runs Avenia Digital Products. The customer buys here and receives a license key by email and under My Account → License Keys.
  • The customer’s site, runs your plugin or theme. Your code asks the customer for that key, then calls your store’s License REST API to activate and periodically validate it.

Your distributed product never talks to Stripe or PayPal. It only talks to your store over HTTPS.

Part 1: Set Up The Product On Your Store

Before writing any code:

  • In Avenia Digital Products → Add Product, create the product customers will buy.
  • Attach the downloadable file (your plugin ZIP, theme ZIP, or installer).
  • In License Settings, check Enable License Keys.
  • In Pricing, set license quantities that match how many sites each plan allows (1, 5, 0 for unlimited, and so on). That quantity becomes the maximum number of activations for keys sold on that tier.
  • If the product is a subscription, set monthly and/or yearly prices. Subscription licenses stay valid while the subscription is active.
  • Publish the product.
  • With Simulate Mode on (or with Stripe/PayPal test credentials), buy the product yourself and copy the license key from the email or My Account.

Your API base URL is:

https://your-store.com/wp-json/avenia-digital-products/v1/

Replace your-store.com with your real domain. Full request and response shapes are in License Rest Api.

Part 2: Add A License Field To Your Plugin Or Theme

Customers need a place to paste their key. The usual pattern is a settings field on your plugin’s settings page, or under Appearance → Customize / Theme Options for a theme.

Below is a minimal example for a plugin. Adapt the option names and menu titles to your product.

Register a settings page and field

<?php
/**
 * Register a simple settings page with a license key field.
 */
add_action( 'admin_menu', 'my_product_add_settings_page' );
add_action( 'admin_init', 'my_product_register_settings' );

function my_product_add_settings_page() {
	add_options_page(
		'My Product License',
		'My Product',
		'manage_options',
		'my-product-license',
		'my_product_render_settings_page'
	);
}

function my_product_register_settings() {
	register_setting(
		'my_product_license_group',
		'my_product_license_key',
		array(
			'type'              => 'string',
			'sanitize_callback' => 'sanitize_text_field',
			'default'           => '',
		)
	);
}

function my_product_render_settings_page() {
	if ( ! current_user_can( 'manage_options' ) ) {
		return;
	}

	// Handle activate / deactivate buttons.
	if ( isset( $_POST['my_product_activate_license'] ) && check_admin_referer( 'my_product_license_action' ) ) {
		$key = isset( $_POST['my_product_license_key'] ) ? sanitize_text_field( wp_unslash( $_POST['my_product_license_key'] ) ) : '';
		update_option( 'my_product_license_key', $key );
		$result = My_Product_License_Client::activate( $key );
		if ( is_wp_error( $result ) ) {
			add_settings_error( 'my_product_license', 'activate_failed', $result->get_error_message(), 'error' );
		} else {
			add_settings_error( 'my_product_license', 'activate_ok', __( 'License activated.', 'my-product' ), 'updated' );
		}
	}

	if ( isset( $_POST['my_product_deactivate_license'] ) && check_admin_referer( 'my_product_license_action' ) ) {
		My_Product_License_Client::deactivate();
		add_settings_error( 'my_product_license', 'deactivate_ok', __( 'License deactivated.', 'my-product' ), 'updated' );
	}

	$license_key = (string) get_option( 'my_product_license_key', '' );
	$status      = (string) get_option( 'my_product_license_status', 'invalid' );
	?>
	<div class="wrap">
		<h1><?php esc_html_e( 'My Product License', 'my-product' ); ?></h1>
		<?php settings_errors( 'my_product_license' ); ?>

		<form method="post" action="">
			<?php wp_nonce_field( 'my_product_license_action' ); ?>
			<table class="form-table" role="presentation">
				<tr>
					<th scope="row">
						<label for="my_product_license_key"><?php esc_html_e( 'License key', 'my-product' ); ?></label>
					</th>
					<td>
						<input
							name="my_product_license_key"
							id="my_product_license_key"
							type="text"
							class="regular-text"
							value="<?php echo esc_attr( $license_key ); ?>"
							placeholder="XXXX-XXXX-XXXX-XXXX"
							autocomplete="off"
						/>
						<p class="description">
							<?php esc_html_e( 'Paste the key from your purchase email or My Account on the store.', 'my-product' ); ?>
						</p>
					</td>
				</tr>
				<tr>
					<th scope="row"><?php esc_html_e( 'Status', 'my-product' ); ?></th>
					<td>
						<?php
						echo 'valid' === $status
							? esc_html__( 'Active', 'my-product' )
							: esc_html__( 'Inactive', 'my-product' );
						?>
					</td>
				</tr>
			</table>
			<p>
				<button type="submit" name="my_product_activate_license" class="button button-primary">
					<?php esc_html_e( 'Activate license', 'my-product' ); ?>
				</button>
				<button type="submit" name="my_product_deactivate_license" class="button">
					<?php esc_html_e( 'Deactivate license', 'my-product' ); ?>
				</button>
			</p>
		</form>
	</div>
	<?php
}

What this gives the customer:

  • A field to paste their key
  • An Activate button that calls your store
  • A Deactivate button when they move sites or cancel
  • A simple Active / Inactive status

You can place the same field under a custom top-level menu, or under your existing settings tabs, as long as only administrators can change it.

Theme options example (sketch)

For a theme, the same idea applies: store the key with update_option(), show an input in your theme options panel, and call activate/deactivate when the user saves or clicks a button. Avoid putting license activation in functions.php on every front-end request, only run it when the user submits the form or on a scheduled check.

Part 3: Talk To The License Api

se the client class below (or your own HTTP code). Set STORE_URL to your store’s home URL and PRODUCT_ID to the numeric ID of the product in Avenia Digital Products (edit the product in wp-admin and check the URL: post.php?post=42&action=edit → PRODUCT_ID is 42).

product_id is required on activate and validate. The store rejects a key that belongs to a different product, so a license sold for Product A cannot unlock Product B.

<?php
class My_Product_License_Client {

	const STORE_URL  = 'https://your-store.com';
        const PRODUCT_ID = 42; // Store product ID for THIS plugin/theme only.


	public static function api_url( $endpoint ) {
		return trailingslashit( self::STORE_URL ) . 'wp-json/avenia-digital-products/v1/' . ltrim( $endpoint, '/' );
	}

	public static function domain() {
		$host = parse_url( home_url(), PHP_URL_HOST );
		return $host ? strtolower( $host ) : '';
	}

	public static function request( $endpoint, $body ) {
		$response = wp_remote_post(
			self::api_url( $endpoint ),
			array(
				'timeout' => 15,
				'headers' => array( 'Content-Type' => 'application/json' ),
				'body'    => wp_json_encode( $body ),
			)
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$code = wp_remote_retrieve_response_code( $response );
		$data = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( 429 === $code ) {
			return new WP_Error( 'rate_limited', __( 'License server is busy. Try again later.', 'my-product' ) );
		}

		if ( ! is_array( $data ) ) {
			return new WP_Error( 'bad_response', __( 'Invalid response from license server.', 'my-product' ) );
		}

		return $data;
	}

	public static function activate( $license_key ) {
		$license_key = sanitize_text_field( $license_key );
		if ( '' === $license_key ) {
			return new WP_Error( 'empty_key', __( 'Enter a license key.', 'my-product' ) );
		}

		$result = self::request(
			'activate-license',
			array(
				'license_key'  => $license_key,
				'domain'       => self::domain(),
				'product_id'   => self::PRODUCT_ID,
				'product_type' => 'wordpress', // or 'plugin', 'theme', etc. (metadata only)
			)
		);

		if ( is_wp_error( $result ) ) {
			return $result;
		}

		if ( empty( $result['success'] ) ) {
			$message = isset( $result['message'] ) ? $result['message'] : __( 'Activation failed.', 'my-product' );
			return new WP_Error( 'activate_failed', $message );
		}

		update_option( 'my_product_license_key', $license_key );
		update_option( 'my_product_activation_key', sanitize_text_field( $result['activation_key'] ) );
		update_option( 'my_product_license_status', 'valid' );
		update_option( 'my_product_license_checked_at', time() );

		return true;
	}

	public static function validate( $force = false ) {
		$key = (string) get_option( 'my_product_license_key', '' );
		if ( '' === $key ) {
			update_option( 'my_product_license_status', 'invalid' );
			return false;
		}

		$checked_at = (int) get_option( 'my_product_license_checked_at', 0 );
		if ( ! $force && $checked_at && ( time() - $checked_at ) < DAY_IN_SECONDS ) {
			return 'valid' === get_option( 'my_product_license_status', 'invalid' );
		}

		$result = self::request(
			'validate-license',
			array(
				'license_key' => $key,
				'product_id'  => self::PRODUCT_ID,
			)
		);

		if ( is_wp_error( $result ) ) {
			// Network blip: keep last known status so paying customers are not locked out.
			return 'valid' === get_option( 'my_product_license_status', 'invalid' );
		}

		$valid = ! empty( $result['valid'] )
			&& isset( $result['product_id'] )
			&& (int) $result['product_id'] === (int) self::PRODUCT_ID;
		update_option( 'my_product_license_status', $valid ? 'valid' : 'invalid' );
		update_option( 'my_product_license_checked_at', time() );

		return $valid;
	}

	public static function deactivate() {
		$key            = (string) get_option( 'my_product_license_key', '' );
		$activation_key = (string) get_option( 'my_product_activation_key', '' );

		if ( $key && $activation_key ) {
			self::request(
				'deactivate-license',
				array(
					'license_key'    => $key,
					'activation_key' => $activation_key,
					'domain'         => self::domain(),
				)
			);
		}

		delete_option( 'my_product_license_key' );
		delete_option( 'my_product_activation_key' );
		delete_option( 'my_product_license_status' );
		delete_option( 'my_product_license_checked_at' );
	}

	public static function is_pro() {
		return self::validate();
	}
}
if ( My_Product_License_Client::is_pro() ) {
	// Load pro modules, unlock settings, etc.
}

Choosing how strictly to enforce the license

Two common product models:

  1. Freemium / feature gate: A base plugin or theme always works; the license unlocks premium modules, settings, or templates.
  2. Paid-only / soft lockout: The product is sold as a full commercial ZIP; without a valid license, main features do not load, but the customer can still reach the license screen and activate a key.

Feature gating (recommended default):

if ( My_Product_License_Client::is_pro() ) {
	// Load pro modules, unlock settings, etc.
}

Soft lockout for a paid-only plugin — load core bootstrap and the license UI always; load the rest of the product only when valid:

add_action( 'plugins_loaded', 'my_product_bootstrap' );

function my_product_bootstrap() {
    // Always load: license client, settings page, activate/deactivate UI.
    require_once __DIR__ . '/includes/class-license-client.php';
    require_once __DIR__ . '/includes/admin-license-page.php';

    if ( ! My_Product_License_Client::is_pro() ) {
        add_action( 'admin_notices', 'my_product_license_required_notice' );
        return;
    }

    // Load the rest of the product only when licensed.
    require_once __DIR__ . '/includes/class-plugin.php';
    My_Product_Plugin::init();
}

function my_product_license_required_notice() {
    if ( ! current_user_can( 'manage_options' ) ) {
        return;
    }
    $url = admin_url( 'options-general.php?page=my-product-license' );
    echo '<div class="notice notice-warning"><p>';
    echo esc_html__( 'My Product requires a valid license key.', 'my-product' );
    echo ' <a href="' . esc_url( $url ) . '">' . esc_html__( 'Activate license', 'my-product' ) . '</a>';
    echo '</p></div>';
}

Soft lockout for a paid-only theme, keep the site usable; do not remove the theme shell or break the front end when the key is missing. Gate premium options, template parts, or demo features instead of exiting from functions.php:

add_action( 'after_setup_theme', 'my_theme_setup_license_gates' );

function my_theme_setup_license_gates() {
    // Always register theme supports, menus, and the license options panel.

    if ( ! My_Product_License_Client::is_pro() ) {
        add_action( 'admin_notices', 'my_theme_license_required_notice' );
        // Skip premium customizer panels, premium template parts, etc.
        return;
    }

    // Register premium features only when licensed.
}

Guardrails for either model:

  • Always leave the license settings UI reachable when locked so the customer can activate or fix a key.
  • Prefer a soft fail (skip loading features + admin notice) over fatal errors, white screens, or deleting output from a theme’s functions.php.
  • Do not call the license API on every front-end request; use the stored status and the daily check in Part 4.

Part 4: Periodic Validation

Do not call the API on every page load. The store rate-limits requests per IP (default 100 per hour, configurable under Settings → Product licenses on the store).

A daily check is enough for most products:

add_action( 'my_product_daily_license_check', array( 'My_Product_License_Client', 'validate' ) );

if ( ! wp_next_scheduled( 'my_product_daily_license_check' ) ) {
	wp_schedule_event( time(), 'daily', 'my_product_daily_license_check' );
}

Pass $force = true to validate( true ) only when the user clicks Activate or you need an immediate refresh.

Part 5: Product Scoping (multi-product stores)

Each license key is stored with the product_id of the item that was purchased. Activate and
validate require that same product_id in the request body.

If you sell two plugins (or two subscription products) from the same store:

  1. Create two products in Avenia Digital Products (e.g. post IDs 42 and 99).
  2. Hardcode PRODUCT_ID = 42 in Plugin A’s client and PRODUCT_ID = 99 in Plugin B’s client.
  3. A key issued for product 42 fails activate/validate when Plugin B sends product_id: 99.

Do not reuse one client class across products without changing PRODUCT_ID. product_type
(wordpress, plugin, theme) is only activation metadata, it does not scope the license.

Part 6: Domain And Site Moves

Activations are tied to a domain string. The example uses the site host from home_url().

  • Activating the same key on the same domain again is safe; it does not consume another slot.
  • A new domain uses a new slot until the maximum is reached.
  • Customers can free a slot from My Account → Installations on your store, or by clicking Deactivate in your settings UI before moving.

Part 7: Subscriptions

If the store product is a subscription, validate-license returns invalid when the subscription is no longer active or access was revoked. Keep daily validation enabled so pro features turn off after cancellation or failed payment.

Part 8: Testing

  • Enable license keys on the product and buy it with Simulate payments on (or with sandbox payment credentials).
  • Copy the key from the order email or My Account. Confirm your client’s PRODUCT_ID matches that product’s ID.
  • On a local or staging site, paste the key and click Activate.
  • Confirm pro features unlock (or the full product loads, for paid-only soft lockout) and status shows Active.
  • Optionally buy a second product and try that key against the first product’s client — activation should fail.
  • Click Deactivate, or revoke the order on the store, then confirm features lock — and that the license settings screen still works.