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
- 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.
- 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).
- 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).
- 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
- Edit the product → Product Details (below the download file fields).
- Check “Offer WordPress plugin/theme updates”.
- Choose Package type: Plugin or Theme.
- 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
- Copy the Update URI into the plugin header or theme style.css.
- Copy the Updates API base URL from System Status (or the product screen) into your updater client.
- Note the product ID (from the product edit URL, e.g. post=123).


Publishing a release
- Bump Version in the plugin header or theme style.css (and any version constant you use).
- Build the zip with the plugin or theme folder at the root of the archive.
- Upload that zip as the product download file.
- Set Current version on the product to the same number.
- Update the changelog text for this release.
- 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.