WordPress Plugin and Theme Updates Guide

This guide is for store owners who sell WordPress plugins or themes through Avenia Digital Products and want buyers to see updates under Dashboard → Updates.

How it works

  1. On your store product, turn on “Offer WordPress plugin/theme updates”, choose Plugin or Theme, set version / slug (and basename for plugins), and keep the product download file as the zip WordPress will install.
  2. Ship a small updater client inside the product you sell and load it on boot:
  • Plugins Updater Sample: Require it from the main plugin file and bootstrap on plugins_loaded (see sample header).
  • Themes Updater Sample: Require it from functions.php and bootstrap on after_setup_theme (see sample header).
  1. When you release a new build, upload the new zip and bump the product’s Current version to match the Version in that zip (plugin header or style.css).
  2. Buyers’ sites check your store on WordPress’s normal update schedule. They still click Update in wp-admin unless they enabled auto-updates.

Free catalog products can offer updates the same way as paid ones.

Plugin vs Theme

Same store API, package tokens, and license rules. Differences:

  • Package type: set Plugin or Theme on the product.
  • Match key: plugins use slug + basename (folder/main-file.php). Themes use theme slug only (folder / stylesheet name). Basename is hidden for themes.
  • Client hooks: plugins use update_plugins / plugins_api; themes use update_themes / themes_api (see the matching sample).
  • Update URI: plugins: main plugin file header. Themes: style.css.
  • Update list image for plugins: optional Update icon on the product, else the featured image (API icons). Themes use screenshot.png (or .jpg) inside the theme folder in the zip; the store featured image is only for your catalog.

License key and updates

License keys are optional and separate from updates:

  • License keys on: get-version requires a valid license key and an activated domain before returning a package URL. Revoked or expired licenses get no package.
  • License keys off: update checks do not require a license.

Keys explained (when licenses are on):

  • license_key: the key the customer received after purchase. Required for updates.
  • domain: the buyer’s site host. Required for updates, and that domain must already be activated.
  • activation_key: returned in the activate-license response on the buyer’s site. Optional for get-version (the store can match by license + domain alone). Still save it after activate and send it when you have it, deactivate needs it, and it’s the more precise token. Your sold product stores it (e.g. update_option); the store does not push it to the buyer site. See License Rest Api and Licensing Integration Guide.

Package URLs expire after a short time. The sample updaters request a fresh package URL when the user clicks Update, so a delayed install still works.

Store setup

  1. Edit the product → Product Details (below the download file fields).
  2. Check “Offer WordPress plugin/theme updates”.
  3. Choose Package type: Plugin or Theme.
  4. Fill in:
  • Current version: must match the Version in the zip (e.g. 1.2.0)
  • Plugin slug or Theme slug: folder name (e.g. my-plugin / my-theme)
  • Plugin basename: plugins only (e.g. my-plugin/my-plugin.php)
  • Update icon: plugins only; falls back to featured image
  • Plugins only: Tested up to (recommended), Requires WordPress, Requires PHP, Changelog
  1. Copy the Update URI into the plugin header or theme style.css.
  2. Copy the Updates API base URL from System Status (or the product screen) into your updater client.
  3. Note the product ID (from the product edit URL, e.g. post=123).

Publishing a release

  1. Bump Version in the plugin header or theme style.css (and any version constant you use).
  2. Build the zip with the plugin or theme folder at the root of the archive.
  3. Upload that zip as the product download file.
  4. Set Current version on the product to the same number.
  5. Update the changelog text for this release.
  6. Save the product.

If only the store version is higher, sites may be offered an update that still installs an older Version header, useful for a quick API test, not for a real release. Always bump both for production.

API endpoints

Base URL (example):

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

Live value: Avenia Digital Products → System Status → Updates API base URL.

POST /get-version
POST /plugin-information

(The plugin-information endpoint also serves theme details modals; the payload includes “type”: “plugin” or “theme”.)

JSON body (always):

product_id (int, required)
current_version (string, optional) — version installed on the buyer’s site

When the product has license keys enabled, also send:

license_key (string, required)
domain (string, required) — site host, e.g. example.com
activation_key (string, optional) — not required for updates if the domain is already activated; recommended when available

Success includes type, new_version, package, download_link, slug, and for plugins “plugin” (basename) or for themes “theme” (slug), plus changelog sections and related fields. Invalid license responses use HTTP 200 with an empty package and a message (same style as the license API). Rate limits are shared with Settings → Product licenses.

Headers

Plugin:

/**
   Plugin Name: My Product
   Version: 1.0.0
   Update URI: https://your-store.com/product/my-product/
*/

Theme style.css:

/*
  Theme Name: My Theme
  Version: 1.0.0
  Update URI: https://your-store.com/product/my-theme/
*/

Update URI only blocks wordpress.org from overwriting your package. Real updates come from your store API via the updater client.

Sample updaters

Plugins: copy the sample updater below into your plugin in a file (e.g. /class-aveniadp-plugin-updater.php) (e.g. includes/), then require and bootstrap it from the main plugin PHP file on plugins_loaded (class AVENIADP_Sample_Plugin_Updater). Pass api_url, product_id, plugin_file, and license fields when required.

<?php
/**
 * Sample WordPress plugin updater client for Avenia Digital Products stores.
 *
 * Copy this file into your sold plugin and load it on plugins_loaded.
 *
 * Licensed products: also integrate license activate/validate (see
 * https://aveniadigitalproducts.com/docs/licensing-integration-guide/) and pass license_key + activation_key.
 *
 * @package YourSoldPlugin
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Example bootstrap (in your main plugin file):
 *
 *   require_once __DIR__ . '/includes/class-aveniadp-plugin-updater.php';
 *   add_action(
 *       'plugins_loaded',
 *       static function () {
 *           new AVENIADP_Sample_Plugin_Updater(
 *               array(
 *                   'api_url'          => 'https://your-store.com/wp-json/avenia-digital-products/v1/',
 *                   'product_id'       => 123,
 *                   'plugin_file'      => __FILE__,
 *                   'license_key'      => (string) get_option( 'my_product_license_key', '' ),
 *                   'activation_key'   => (string) get_option( 'my_product_activation_key', '' ),
 *                   'requires_license' => true,
 *               )
 *           );
 *       }
 *   );
 */
class AVENIADP_Sample_Plugin_Updater {

	/**
	 * Settings.
	 *
	 * @var array
	 */
	private $config;

	/**
	 * Plugin basename (folder/file.php).
	 *
	 * @var string
	 */
	private $plugin_basename;

	/**
	 * Constructor.
	 *
	 * @param array $config {
	 *     @type string $api_url          Store REST base ending with /v1/.
	 *     @type int    $product_id       Store product ID.
	 *     @type string $plugin_file      Absolute path to main plugin file.
	 *     @type string $license_key      Optional license key.
	 *     @type string $activation_key   Optional activation key.
	 *     @type bool   $requires_license Whether to send license fields.
	 *     @type int    $cache_ttl        Seconds to cache API responses. Default 12 hours.
	 * }
	 */
	public function __construct( $config ) {
		$this->config          = wp_parse_args(
			$config,
			array(
				'api_url'          => '',
				'product_id'       => 0,
				'plugin_file'      => '',
				'license_key'      => '',
				'activation_key'   => '',
				'requires_license' => false,
				'cache_ttl'        => 12 * HOUR_IN_SECONDS,
			)
		);
		$this->plugin_basename = plugin_basename( $this->config['plugin_file'] );

		add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_update' ) );
		add_filter( 'plugins_api', array( $this, 'plugins_api' ), 10, 3 );
		add_filter( 'upgrader_pre_download', array( $this, 'pre_download' ), 10, 3 );
	}

	/**
	 * Inject update into site transient.
	 *
	 * @param object $transient Transient.
	 * @return object
	 */
	public function check_update( $transient ) {
		if ( empty( $transient ) || ! is_object( $transient ) ) {
			$transient = new stdClass();
		}
		if ( empty( $transient->checked ) || ! is_array( $transient->checked ) ) {
			return $transient;
		}

		$current = isset( $transient->checked[ $this->plugin_basename ] )
			? $transient->checked[ $this->plugin_basename ]
			: $this->get_installed_version();

		$response = $this->api_request( 'get-version', $current );
		if ( is_wp_error( $response ) || empty( $response['new_version'] ) ) {
			return $transient;
		}

		if ( version_compare( $response['new_version'], $current, '>' ) && ! empty( $response['package'] ) ) {
			$plugin              = new stdClass();
			$plugin->slug        = ! empty( $response['slug'] ) ? $response['slug'] : dirname( $this->plugin_basename );
			$plugin->plugin      = ! empty( $response['plugin'] ) ? $response['plugin'] : $this->plugin_basename;
			$plugin->new_version = $response['new_version'];
			$plugin->url         = isset( $response['url'] ) ? $response['url'] : '';
			$plugin->package     = $response['package'];
			$plugin->icons       = isset( $response['icons'] ) && is_array( $response['icons'] ) ? $response['icons'] : array();
			if ( ! empty( $response['tested'] ) ) {
				$plugin->tested = $response['tested'];
			}
			if ( ! empty( $response['requires'] ) ) {
				$plugin->requires = $response['requires'];
			}
			if ( ! empty( $response['requires_php'] ) ) {
				$plugin->requires_php = $response['requires_php'];
			}

			$transient->response[ $this->plugin_basename ] = $plugin;
		} else {
			unset( $transient->response[ $this->plugin_basename ] );
		}

		return $transient;
	}

	/**
	 * Provide plugin information modal data.
	 *
	 * @param false|object|array $result Result.
	 * @param string             $action Action.
	 * @param object             $args   Args.
	 * @return false|object|array
	 */
	public function plugins_api( $result, $action, $args ) {
		if ( 'plugin_information' !== $action ) {
			return $result;
		}

		$slug = dirname( $this->plugin_basename );
		if ( empty( $args->slug ) || $args->slug !== $slug ) {
			return $result;
		}

		$response = $this->api_request( 'plugin-information', $this->get_installed_version() );
		if ( is_wp_error( $response ) || empty( $response['new_version'] ) ) {
			return $result;
		}

		$info                = new stdClass();
		$info->name          = isset( $response['name'] ) ? $response['name'] : $slug;
		$info->slug          = isset( $response['slug'] ) ? $response['slug'] : $slug;
		$info->version       = $response['new_version'];
		$info->author        = '';
		$info->homepage      = isset( $response['homepage'] ) ? $response['homepage'] : '';
		$info->download_link = isset( $response['download_link'] ) ? $response['download_link'] : '';
		$info->sections      = isset( $response['sections'] ) && is_array( $response['sections'] ) ? $response['sections'] : array();
		$info->banners       = isset( $response['banners'] ) ? $response['banners'] : array();
		$info->icons         = isset( $response['icons'] ) && is_array( $response['icons'] ) ? $response['icons'] : array();
		if ( ! empty( $response['tested'] ) ) {
			$info->tested = $response['tested'];
		}
		if ( ! empty( $response['requires'] ) ) {
			$info->requires = $response['requires'];
		}
		if ( ! empty( $response['requires_php'] ) ) {
			$info->requires_php = $response['requires_php'];
		}

		return $info;
	}

	/**
	 * Request a fresh package URL when the user clicks Update.
	 *
	 * @param bool|string|WP_Error $reply    Filter value.
	 * @param string               $package  Package URL.
	 * @param WP_Upgrader          $upgrader Upgrader instance.
	 * @return bool|string|WP_Error
	 */
	public function pre_download( $reply, $package, $upgrader ) {
		unset( $upgrader );

		if ( false !== $reply ) {
			return $reply;
		}

		if ( ! $this->is_our_package( $package ) ) {
			return $reply;
		}

		$response = $this->api_request( 'get-version', $this->get_installed_version(), false );
		if ( is_wp_error( $response ) ) {
			return $response;
		}
		if ( empty( $response['package'] ) ) {
			return new WP_Error( 'aveniadp_update_package', 'Could not retrieve a fresh update package.' );
		}

		$transient = get_site_transient( 'update_plugins' );
		if ( is_object( $transient ) && ! empty( $transient->response[ $this->plugin_basename ] ) ) {
			$transient->response[ $this->plugin_basename ]->package = $response['package'];
			set_site_transient( 'update_plugins', $transient );
		}

		require_once ABSPATH . 'wp-admin/includes/file.php';

		$downloaded = download_url( $response['package'] );
		if ( is_wp_error( $downloaded ) ) {
			return $downloaded;
		}

		return $downloaded;
	}

	/**
	 * Whether this package URL is for this plugin.
	 *
	 * @param string $package Package URL.
	 * @return bool
	 */
	private function is_our_package( $package ) {
		$package = (string) $package;
		if ( '' === $package ) {
			return false;
		}

		$transient = get_site_transient( 'update_plugins' );
		if ( is_object( $transient ) && ! empty( $transient->response[ $this->plugin_basename ]->package ) ) {
			return (string) $transient->response[ $this->plugin_basename ]->package === $package;
		}

		return false !== strpos( $package, 'aveniadp_update_download=' )
			|| false !== strpos( $package, '/aveniadp-update-download/' );
	}

	/**
	 * Call store update API.
	 *
	 * @param string $endpoint        Endpoint slug.
	 * @param string $current_version Installed version.
	 * @param bool   $use_cache       Whether to use a cached response.
	 * @return array|WP_Error
	 */
	private function api_request( $endpoint, $current_version, $use_cache = true ) {
		$cache_key = 'aveniadp_upd_' . md5( $endpoint . '|' . $this->config['product_id'] . '|' . $current_version . '|' . $this->config['license_key'] );

		if ( $use_cache ) {
			$cached = get_site_transient( $cache_key );
			if ( is_array( $cached ) ) {
				return $cached;
			}
		}

		$body = array(
			'product_id'      => absint( $this->config['product_id'] ),
			'current_version' => (string) $current_version,
		);

		if ( ! empty( $this->config['requires_license'] ) ) {
			$body['license_key']    = (string) $this->config['license_key'];
			$body['domain']         = $this->get_domain();
			$body['activation_key'] = (string) $this->config['activation_key'];
		}

		$url = trailingslashit( $this->config['api_url'] ) . ltrim( $endpoint, '/' );

		$response = wp_remote_post(
			$url,
			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 ( 200 !== (int) $code || ! is_array( $data ) ) {
			return new WP_Error( 'aveniadp_update_api', 'Unexpected update API response.' );
		}

		if ( $use_cache ) {
			set_site_transient( $cache_key, $data, absint( $this->config['cache_ttl'] ) );
		}

		return $data;
	}

	/**
	 * Installed plugin version from headers.
	 *
	 * @return string
	 */
	private function get_installed_version() {
		if ( ! function_exists( 'get_plugin_data' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
		$data = get_plugin_data( $this->config['plugin_file'], false, false );
		return isset( $data['Version'] ) ? (string) $data['Version'] : '0';
	}

	/**
	 * Site domain for license-bound update checks.
	 *
	 * @return string
	 */
	private function get_domain() {
		$host = wp_parse_url( home_url(), PHP_URL_HOST );
		return is_string( $host ) ? $host : '';
	}
}


Themes: copy updater below in a file (e.g. /class-aveniadp-theme-updater.php) into your theme (e.g. inc/), then require and bootstrap it from functions.php on after_setup_theme (class AVENIADP_Sample_Theme_Updater). Pass api_url, product_id, theme_slug (usually get_template()), and license fields when required.

<?php
/**
 * Sample WordPress theme updater client for Avenia Digital Products stores.
 *
 * Copy this file into your sold theme (e.g. inc/) and load it from functions.php.
 *
 * Licensed products: also integrate license activate/validate (see
 * https://aveniadigitalproducts.com/docs/licensing-integration-guide/) and pass license_key + activation_key.
 *
 * @package YourSoldTheme
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Example bootstrap (in functions.php):
 *
 *   require_once get_template_directory() . '/inc/class-aveniadp-theme-updater.php';
 *   add_action(
 *       'after_setup_theme',
 *       static function () {
 *           new AVENIADP_Sample_Theme_Updater(
 *               array(
 *                   'api_url'          => 'https://your-store.com/wp-json/avenia-digital-products/v1/',
 *                   'product_id'       => 123,
 *                   'theme_slug'       => get_template(), // parent theme folder name
 *                   'license_key'      => (string) get_option( 'my_theme_license_key', '' ),
 *                   'activation_key'   => (string) get_option( 'my_theme_activation_key', '' ),
 *                   'requires_license' => true,
 *               )
 *           );
 *       }
 *   );
 *
 * style.css should include:
 *   Update URI: https://your-store.com/product/my-theme/
 */
class AVENIADP_Sample_Theme_Updater {

	/**
	 * Settings.
	 *
	 * @var array
	 */
	private $config;

	/**
	 * Theme stylesheet slug (folder name).
	 *
	 * @var string
	 */
	private $theme_slug;

	/**
	 * Constructor.
	 *
	 * @param array $config {
	 *     @type string $api_url          Store REST base ending with /v1/.
	 *     @type int    $product_id       Store product ID.
	 *     @type string $theme_slug       Theme folder / stylesheet slug.
	 *     @type string $license_key      Optional license key.
	 *     @type string $activation_key   Optional activation key.
	 *     @type bool   $requires_license Whether to send license fields.
	 *     @type int    $cache_ttl        Seconds to cache API responses. Default 12 hours.
	 * }
	 */
	public function __construct( $config ) {
		$this->config     = wp_parse_args(
			$config,
			array(
				'api_url'          => '',
				'product_id'       => 0,
				'theme_slug'       => '',
				'license_key'      => '',
				'activation_key'   => '',
				'requires_license' => false,
				'cache_ttl'        => 12 * HOUR_IN_SECONDS,
			)
		);
		$this->theme_slug = sanitize_title( (string) $this->config['theme_slug'] );
		if ( '' === $this->theme_slug ) {
			$this->theme_slug = get_template();
		}

		add_filter( 'pre_set_site_transient_update_themes', array( $this, 'check_update' ) );
		add_filter( 'themes_api', array( $this, 'themes_api' ), 10, 3 );
		add_filter( 'upgrader_pre_download', array( $this, 'pre_download' ), 10, 3 );
	}

	/**
	 * Inject update into themes transient.
	 *
	 * @param object $transient Transient.
	 * @return object
	 */
	public function check_update( $transient ) {
		if ( empty( $transient ) || ! is_object( $transient ) ) {
			$transient = new stdClass();
		}
		if ( empty( $transient->checked ) || ! is_array( $transient->checked ) ) {
			return $transient;
		}

		$current = isset( $transient->checked[ $this->theme_slug ] )
			? $transient->checked[ $this->theme_slug ]
			: $this->get_installed_version();

		$response = $this->api_request( 'get-version', $current );
		if ( is_wp_error( $response ) || empty( $response['new_version'] ) ) {
			return $transient;
		}

		if ( version_compare( $response['new_version'], $current, '>' ) && ! empty( $response['package'] ) ) {
			$update = array(
				'theme'       => ! empty( $response['theme'] ) ? $response['theme'] : $this->theme_slug,
				'new_version' => $response['new_version'],
				'url'         => isset( $response['url'] ) ? $response['url'] : '',
				'package'     => $response['package'],
			);
			if ( ! empty( $response['requires'] ) ) {
				$update['requires'] = $response['requires'];
			}
			if ( ! empty( $response['requires_php'] ) ) {
				$update['requires_php'] = $response['requires_php'];
			}

			$transient->response[ $this->theme_slug ] = $update;
		} else {
			unset( $transient->response[ $this->theme_slug ] );
		}

		return $transient;
	}

	/**
	 * Provide theme information modal data.
	 *
	 * @param false|object|array $result Result.
	 * @param string             $action Action.
	 * @param object             $args   Args.
	 * @return false|object|array
	 */
	public function themes_api( $result, $action, $args ) {
		if ( 'theme_information' !== $action ) {
			return $result;
		}

		if ( empty( $args->slug ) || $args->slug !== $this->theme_slug ) {
			return $result;
		}

		// Same store endpoint as plugins; payload includes type=theme.
		$response = $this->api_request( 'plugin-information', $this->get_installed_version() );
		if ( is_wp_error( $response ) || empty( $response['new_version'] ) ) {
			return $result;
		}

		$info                = new stdClass();
		$info->name          = isset( $response['name'] ) ? $response['name'] : $this->theme_slug;
		$info->slug          = isset( $response['slug'] ) ? $response['slug'] : $this->theme_slug;
		$info->version       = $response['new_version'];
		$info->author        = '';
		$info->homepage      = isset( $response['homepage'] ) ? $response['homepage'] : '';
		$info->download_link = isset( $response['download_link'] ) ? $response['download_link'] : '';
		$info->sections      = isset( $response['sections'] ) && is_array( $response['sections'] ) ? $response['sections'] : array();
		if ( ! empty( $response['requires'] ) ) {
			$info->requires = $response['requires'];
		}
		if ( ! empty( $response['requires_php'] ) ) {
			$info->requires_php = $response['requires_php'];
		}

		return $info;
	}

	/**
	 * Request a fresh package URL when the user clicks Update.
	 *
	 * @param bool|string|WP_Error $reply    Filter value.
	 * @param string               $package  Package URL.
	 * @param WP_Upgrader          $upgrader Upgrader instance.
	 * @return bool|string|WP_Error
	 */
	public function pre_download( $reply, $package, $upgrader ) {
		unset( $upgrader );

		if ( false !== $reply ) {
			return $reply;
		}

		if ( ! $this->is_our_package( $package ) ) {
			return $reply;
		}

		$response = $this->api_request( 'get-version', $this->get_installed_version(), false );
		if ( is_wp_error( $response ) ) {
			return $response;
		}
		if ( empty( $response['package'] ) ) {
			return new WP_Error( 'aveniadp_update_package', 'Could not retrieve a fresh update package.' );
		}

		$transient = get_site_transient( 'update_themes' );
		if ( is_object( $transient ) && ! empty( $transient->response[ $this->theme_slug ] ) ) {
			if ( is_array( $transient->response[ $this->theme_slug ] ) ) {
				$transient->response[ $this->theme_slug ]['package'] = $response['package'];
			} else {
				$transient->response[ $this->theme_slug ]->package = $response['package'];
			}
			set_site_transient( 'update_themes', $transient );
		}

		require_once ABSPATH . 'wp-admin/includes/file.php';

		$downloaded = download_url( $response['package'] );
		if ( is_wp_error( $downloaded ) ) {
			return $downloaded;
		}

		return $downloaded;
	}

	/**
	 * Whether this package URL is for this theme.
	 *
	 * @param string $package Package URL.
	 * @return bool
	 */
	private function is_our_package( $package ) {
		$package = (string) $package;
		if ( '' === $package ) {
			return false;
		}

		$transient = get_site_transient( 'update_themes' );
		if ( is_object( $transient ) && ! empty( $transient->response[ $this->theme_slug ] ) ) {
			$entry = $transient->response[ $this->theme_slug ];
			$known = is_array( $entry )
				? ( isset( $entry['package'] ) ? (string) $entry['package'] : '' )
				: ( isset( $entry->package ) ? (string) $entry->package : '' );
			if ( '' !== $known ) {
				return $known === $package;
			}
		}

		return false !== strpos( $package, 'aveniadp_update_download=' )
			|| false !== strpos( $package, '/aveniadp-update-download/' );
	}

	/**
	 * Call store update API.
	 *
	 * @param string $endpoint        Endpoint slug.
	 * @param string $current_version Installed version.
	 * @param bool   $use_cache       Whether to use a cached response.
	 * @return array|WP_Error
	 */
	private function api_request( $endpoint, $current_version, $use_cache = true ) {
		$cache_key = 'aveniadp_theme_upd_' . md5( $endpoint . '|' . $this->config['product_id'] . '|' . $current_version . '|' . $this->config['license_key'] );

		if ( $use_cache ) {
			$cached = get_site_transient( $cache_key );
			if ( is_array( $cached ) ) {
				return $cached;
			}
		}

		$body = array(
			'product_id'      => absint( $this->config['product_id'] ),
			'current_version' => (string) $current_version,
		);

		if ( ! empty( $this->config['requires_license'] ) ) {
			$body['license_key']    = (string) $this->config['license_key'];
			$body['domain']         = $this->get_domain();
			$body['activation_key'] = (string) $this->config['activation_key'];
		}

		$url = trailingslashit( $this->config['api_url'] ) . ltrim( $endpoint, '/' );

		$response = wp_remote_post(
			$url,
			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 ( 200 !== (int) $code || ! is_array( $data ) ) {
			return new WP_Error( 'aveniadp_update_api', 'Unexpected update API response.' );
		}

		if ( $use_cache ) {
			set_site_transient( $cache_key, $data, absint( $this->config['cache_ttl'] ) );
		}

		return $data;
	}

	/**
	 * Installed theme version from style.css.
	 *
	 * @return string
	 */
	private function get_installed_version() {
		$theme = wp_get_theme( $this->theme_slug );
		if ( $theme->exists() ) {
			return (string) $theme->get( 'Version' );
		}
		return '0';
	}

	/**
	 * Site domain for license-bound update checks.
	 *
	 * @return string
	 */
	private function get_domain() {
		$host = wp_parse_url( home_url(), PHP_URL_HOST );
		return is_string( $host ) ? $host : '';
	}
}


Both samples:

  • Check for updates on WordPress’s schedule
  • Fill “View details” where applicable
  • Request a fresh package URL when the user clicks Update

For license activate / validate UI, see Licensing Integration Guide.

What buyers see

  • An update notice under Dashboard → Updates (Plugins or Themes) when a newer store version is available and (if licensed) their license is valid.
  • They click Update unless WordPress auto-updates are enabled for that item.
  • They can always re-download the latest zip from My Account → Downloads.