Skip to content

The update API for your plugin

The plugins you sell call a small REST API on your store, under wp-json/elm/v2/.

The quickest way is the ready-made updater: WooCommerce › Licenses › Settings › License API › Download updater gives you class-elm-plugin-updater.php, one file with no dependencies (PHP 7.2+). Rename the class, create it with your store’s API address, your plugin’s main file and its version, and call activate( $key ) and deactivate() from your settings screen. It hooks WordPress’s update checks and “View details”, caches the answer for 12 hours, an hour when the store refused, or ten minutes when the store didn’t answer, and asks again before downloading, because download links last an hour. It also asks again when the site’s owner chooses Check again under Dashboard › Updates, and it names your plugin when it activates and checks for updates (plugin, below). Instructions are in its header, whose example already has your store’s API address.

Settings › License API: the endpoint address with a Copy button, Download updater, requests per minute and trusted proxies.

1 Your store’s API address. 2 Download updater.

Send a per-site instance_id on every call: generate a UUID once per site and store it in an option. It’s what makes the site limit hold.

Method Endpoint Send Answer
POST /activate key, instance_id, domain; optional plugin Takes a seat for this site. Activating again is fine.
POST /validate key, instance_id (or domain) valid is true only while the license works and this site holds a seat.
POST /update key, current_version, instance_id (or domain); optional wp_version, php_version, channel (stable or beta), plugin update_available, version, changelog, support_active, a download_url valid for one hour, renew_url, icons, and the fields WordPress’s “View details” shows.
POST /deactivate key, instance_id Frees this site’s seat.

/update gives a download link only to a license that works, whose updates period hasn’t ended, from a site that holds a seat. When the updates period has ended it still reports the new version, with download_url: null, so the site can ask its owner to renew. A site that sends neither instance_id nor domain also gets the version with download_url: null, and a message to activate the license first.

Send plugin, your plugin’s main file name such as my-plugin.php. The store reads which plugin files the product’s zip holds, and refuses a key that was sold for another product (wrong_product) before any seat is taken. Without it, a customer who owns two of your plugins could paste one’s key into the other and be offered the wrong update. The drop-in updater sends it for you, and stops an update whose package doesn’t hold your plugin’s main file.

tested answers the site’s own WordPress version when it’s in the branch you entered, so “Tested up to 7.1” reads as compatible on 7.1.3. icons is the product’s image, shown next to the update on Dashboard › Updates. If your CDN or host blocks images requested from other sites (hotlink protection), allow your product images, or leave the icon out with add_filter( 'elm_update_response', fn ( $data ) => [ 'icons' => [] ] + $data );.

Terminal window
curl -X POST https://yourstore.com/wp-json/elm/v2/activate \
-H "Content-Type: application/json" \
-d '{ "key": "XXXX-XXXX-XXXX-XXXX", "domain": "customer-site.com", "instance_id": "8f14e45f-…" }'

Failures answer { "code", "message", "status" }. Branch on code: it never changes, while message is in the store’s language.

code HTTP Meaning
invalid_license 404 No license has this key.
invalid_request 400 Empty domain or instance ID.
license_inactive 403 Refunded, revoked, or otherwise not usable.
license_expired 403 Past its expiry date; renewing fixes it.
wrong_product 403 The key was sold for another product than the plugin asking (plugin). The message names the key’s product. No seat is taken.
activation_limit_reached 403 Every seat is taken; data.limit and data.active say how many.
not_activated 403 The site named itself but holds no seat. /validate answers this with 200 and valid: false instead.
busy 409 Another activation of the same license is running; retry in a moment.
activation_failed 500 The seat couldn’t be saved; retry.
not_found 404 No such license or activation (/deactivate).
rate_limit_exceeded 429 Too many requests; wait Retry-After seconds.

The rate limit is per license key, and per IP address only for keys that don’t exist, so an agency’s sites behind one address don’t share one allowance. If your store sits behind a proxy, list it in Settings so X-Forwarded-For is trusted.

Actions fire once, when a change is saved, with the license row (never the plain key): elm_license_issued, elm_license_activated, elm_license_deactivated, elm_license_revoked, elm_license_restored, elm_license_edited, elm_license_renewed and elm_license_expired. elm_licenses_refunded fires with the order ID and the revoked licenses’ IDs, and elm_license_renewal_refunded when a refunded renewal takes a year back.

Filters: elm_validate_response, elm_update_response, elm_site_is_free, elm_license_auto_renewal, elm_license_key_format (the format of generated keys), elm_customer_licenses (a customer’s licenses, for your own account pages), and elm_write_htaccess_rule, elm_htaccess_file and elm_htaccess_folder_file (the download-protection rule).

Checked against ZealDen Sell Your Plugins 1.1.4 on main, 19 October 2026.