Skip to content
pluginloom

Developers

Licensing and updates API for plugin developers

How a PluginLoom plugin activates its license key, checks status and receives WordPress updates. Endpoints, payloads, errors and a drop-in PHP updater class.

Last updated by the PluginLoom team

This page is for the developer who builds a PluginLoom plugin such as BankBridge. It describes the four endpoints the plugin calls and gives a drop-in PHP class that wires them into the WordPress updater.

In short: the plugin sends the license key and the site URL to activate once, calls check about once a day, calls deactivate when the user removes the key, and lets WordPress ask update for new versions. Downloads are signed URLs that expire.

Base URL and format#

ItemValue
Base URLhttps://pluginloom.com/api/v1
Request bodyJSON (Content-Type: application/json) or form encoded
ResponseJSON, always with a boolean success
Rate limit30 requests per minute per IP for license calls, 60 for update. Over the limit returns HTTP 429 with Retry-After.
AuthenticationThe license key itself. No cookies, no API key.

Activate a site#

POST /licenses/activate

FieldRequiredNotes
license_keyyesFor example BB-7K2QD-9XWMA-3RTNE-H4ZPC
site_urlyeshome_url()
instancenoA random ID you generate once and store in an option, so a cloned database can be told apart
plugin_version, wp_version, php_versionnoShown to support staff
json
{
  "success": true,
  "status": "active",
  "activated": true,
  "site": "example.com",
  "license": {
    "status": "active",
    "expires_at": "2027-10-23T14:02:11.000Z",
    "site_limit": 5,
    "unlimited": false,
    "activations": 2,
    "activations_left": 3
  },
  "product": "bankbridge",
  "plan": "5 sites",
  "message": "License activated for example.com."
}

Activating a site that is already active on the key succeeds and only refreshes lastCheckAt, so it is safe to call again.

Check a license#

POST /licenses/check with license_key and site_url. Returns the same shape with activated true or false for that site. Call it at most once a day (a daily cron event or a 24 hour transient).

Deactivate a site#

POST /licenses/deactivate with license_key and site_url. Frees one activation. Customers can also do this from their account.

Errors#

HTTPerrorMeaningWhat the plugin should show
400invalid_requestMissing or malformed key or site URLAsk the user to check the key
404invalid_keyNo such key"This license key was not found."
403expiredPast the expiry date and grace periodOffer the renew link from renew_url
403revokedRevoked by staffPoint to support
403refundedThe order was refundedPoint to the store
403site_limit_reachedAll activations are in useLink to the account page to free a site
429rate_limitedToo many requestsRetry after Retry-After seconds

An expired, revoked or refunded key never stops the plugin from running. The code is GPL. Only updates and support stop.

Updates for WordPress#

GET /update?slug=bankbridge&version=1.2.0&key=LICENCE&site=example.com

The response follows the fields WordPress expects in update_plugins and plugins_api:

json
{
  "success": true,
  "slug": "bankbridge",
  "name": "BankBridge",
  "version": "1.3.0",
  "new_version": "1.3.0",
  "update_available": true,
  "package": "https://pluginloom.com/api/v1/download?v=...&l=...&e=...&s=...",
  "download_url": "https://pluginloom.com/api/v1/download?v=...",
  "url": "https://pluginloom.com/plugins/bankbridge",
  "tested": "6.9",
  "requires": "6.4",
  "requires_php": "8.1",
  "last_updated": "2026-11-02 10:00:00",
  "sections": { "description": "<p>...</p>", "installation": "<p>...</p>", "changelog": "<h4>1.3.0</h4>..." },
  "license_status": "active"
}

package is a signed link valid for 24 hours. It is empty when the key is missing, expired, revoked or refunded; in that case upgrade_notice explains why, and WordPress shows the update without an install button.

Drop-in updater class#

Save as includes/class-pluginloom-updater.php in the plugin and create it from the main plugin file:

php
new PluginLoom_Updater( __FILE__, 'bankbridge', BANKBRIDGE_VERSION, get_option( 'bankbridge_license_key', '' ) );
php
<?php
/**
 * PluginLoom license + update client. GPL-2.0-or-later.
 */
if ( ! defined( 'ABSPATH' ) ) { exit; }

class PluginLoom_Updater {
	const API = 'https://pluginloom.com/api/v1';

	private $file; private $slug; private $version; private $key; private $basename; private $cache_key;

	public function __construct( $file, $slug, $version, $key ) {
		$this->file      = $file;
		$this->slug      = $slug;
		$this->version   = $version;
		$this->key       = trim( (string) $key );
		$this->basename  = plugin_basename( $file );
		$this->cache_key = 'pluginloom_upd_' . md5( $slug . $this->key );

		add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_update' ) );
		add_filter( 'plugins_api', array( $this, 'plugin_info' ), 20, 3 );
		add_action( 'upgrader_process_complete', array( $this, 'purge' ), 10, 0 );
	}

	/** Activate, check or deactivate. Returns the decoded body or WP_Error. */
	public function license( $action, $key = null ) {
		$res = wp_remote_post( self::API . '/licenses/' . $action, array(
			'timeout' => 15,
			'headers' => array( 'Content-Type' => 'application/json' ),
			'body'    => wp_json_encode( array(
				'license_key'    => $key ? $key : $this->key,
				'site_url'       => home_url(),
				'instance'       => $this->instance(),
				'plugin_version' => $this->version,
				'wp_version'     => get_bloginfo( 'version' ),
				'php_version'    => PHP_VERSION,
			) ),
		) );
		if ( is_wp_error( $res ) ) { return $res; }
		return json_decode( wp_remote_retrieve_body( $res ), true );
	}

	private function instance() {
		$id = get_option( 'pluginloom_instance_' . $this->slug );
		if ( ! $id ) { $id = wp_generate_uuid4(); update_option( 'pluginloom_instance_' . $this->slug, $id, false ); }
		return $id;
	}

	private function remote() {
		$data = get_transient( $this->cache_key );
		if ( false !== $data ) { return $data; }
		$url = add_query_arg( array(
			'slug'    => $this->slug,
			'version' => $this->version,
			'key'     => $this->key,
			'site'    => home_url(),
		), self::API . '/update' );
		$res = wp_remote_get( $url, array( 'timeout' => 15 ) );
		if ( is_wp_error( $res ) || 200 !== wp_remote_retrieve_response_code( $res ) ) {
			set_transient( $this->cache_key, null, HOUR_IN_SECONDS ); // back off on failure
			return null;
		}
		$data = json_decode( wp_remote_retrieve_body( $res ) );
		set_transient( $this->cache_key, $data, 12 * HOUR_IN_SECONDS );
		return $data;
	}

	public function check_update( $transient ) {
		if ( empty( $transient->checked ) ) { return $transient; }
		$r = $this->remote();
		if ( ! $r || empty( $r->new_version ) ) { return $transient; }
		$item = (object) array(
			'id'            => $this->basename,
			'slug'          => $this->slug,
			'plugin'        => $this->basename,
			'new_version'   => $r->new_version,
			'url'           => $r->url,
			'package'       => $r->package,
			'tested'        => $r->tested,
			'requires'      => $r->requires,
			'requires_php'  => $r->requires_php,
			'upgrade_notice'=> isset( $r->upgrade_notice ) ? $r->upgrade_notice : '',
		);
		if ( version_compare( $this->version, $r->new_version, '<' ) ) {
			$transient->response[ $this->basename ] = $item;
		} else {
			$transient->no_update[ $this->basename ] = $item;
		}
		return $transient;
	}

	public function plugin_info( $result, $action, $args ) {
		if ( 'plugin_information' !== $action || empty( $args->slug ) || $args->slug !== $this->slug ) { return $result; }
		$r = $this->remote();
		if ( ! $r ) { return $result; }
		return (object) array(
			'name'          => $r->name,
			'slug'          => $r->slug,
			'version'       => $r->new_version,
			'tested'        => $r->tested,
			'requires'      => $r->requires,
			'requires_php'  => $r->requires_php,
			'last_updated'  => $r->last_updated,
			'homepage'      => $r->url,
			'download_link' => $r->package,
			'sections'      => (array) $r->sections,
		);
	}

	public function purge() { delete_transient( $this->cache_key ); }
}

Downloads#

GET /download?v=...&l=...&e=...&s=... returns the zip. The s parameter is an HMAC of the version, license and expiry, so a link cannot be edited to fetch another version or extended. Every download is logged against the license. Links from the customer account last 10 minutes; links in update responses last 24 hours.

Was this page helpful?