License API Reference
The drop-in client covers most products, but sometimes you need to talk to the license server yourself: a desktop app, a SaaS signup that checks a key, a build script, or a plugin with its own settings framework. For that you need the exact routes, parameters and answers.
Software Licensing registers seven public REST routes on your store. The license key is the credential, so there’s no WordPress login or application password involved. Every route is rate limited, and an unknown key always gets the same answer, so keys can’t be guessed by watching responses.
Base Address and Common Rules
All routes live in the stackless/v1 namespace:
https://yourstore.com/wp-json/stackless/v1/licenses/{route}
Stackless > Settings > Software Licensing shows your exact address under License API and updates.
- Parameters: send them as form fields for POST (recommended, and what the client does) or as a query string for GET. A query string puts the license key in your server’s access logs.
- **
license_key:** case, spaces and dashes don’t matter. Fewer than 16 letters and numbers counts as an unknown key. - **
site_url:** normalized before use.https://www.Example.com/andexample.comare the same site, while a subfolder install is its own site. - **
slugandproduct_id:** optional on most routes. When you send them, they must match the key’s product, or you getinvalid_license.product_idcan be the parent product or the purchased variation. - Caching: answers from activate, deactivate, check, version and info carry
Cache-Control: no-store, private.
Response Shape
A success looks like this:
{
"success": true,
"code": "activated",
"message": "Site activated.",
...route fields
}
An error from a route has success: false, a code, a message and sometimes a license object with the license details:
{
"success": false,
"code": "activation_limit_reached",
"message": "This license has reached its site limit. Deactivate another site or upgrade the license.",
"license": { ...license object }
}
Rate-limit and download errors come back in WordPress’s standard REST error shape instead: code, message and data.status.
The License Object
Activate, deactivate and check return these fields at the top level. Version returns them under license.
status:active,inactive,expiredorrevoked. A key past its expiry date reportsexpiredstraight away.product_idandvariation_id: the purchased product and variation (0 for none).product_name: the product or variation name.expires_at: ISO 8601 date in UTC, ornullfor lifetime.lifetime:truewhen there’s no expiry.activation_limit: 0 means unlimited.activations_count: sites counted against the limit. Development sites aren’t counted while Development sites is on.activations_left: remaining slots, ornullwhen unlimited.
When you send site_url, three more fields appear:
site: the normalized address.site_active: whether that site is activated on this key.is_local: whether the store treats it as a development site.
A request that names an activated site also updates that site’s Last check time in Stackless > Licenses.
POST /licenses/activate
Activates a site. Activating a site that’s already active succeeds and refreshes it.
- Parameters:
license_keyandsite_urlrequired.instance_id,slugandproduct_idoptional. - Success: code
activatedoralready_active, the license object with site fields, andactivation_id. - Errors:
missing_license(400),invalid_license(403),license_expired,license_revokedorlicense_inactive(403),invalid_site(400),activation_limit_reached(409),activation_failed(403).
curl -X POST https://yourstore.com/wp-json/stackless/v1/licenses/activate \
-d license_key=GTP-ABCDE-FGHJK-MNPQR-STUVW \
-d site_url=https://client-site.com \
-d slug=my-plugin
The key must be active to activate a site. A filter, stackless_software_licensing_allow_activation, can refuse an activation with your own error code (returned with 403).
POST /licenses/deactivate
Frees a site’s activation. Expired and revoked keys can still deactivate.
- Parameters:
license_keyandsite_urlrequired.slugandproduct_idoptional. - Success: code
deactivatedand the license object. - Errors:
missing_license(400),invalid_license(403),invalid_site(400),site_not_active(404).
curl -X POST https://yourstore.com/wp-json/stackless/v1/licenses/deactivate \
-d license_key=GTP-ABCDE-FGHJK-MNPQR-STUVW \
-d site_url=https://client-site.com \
-d slug=my-plugin
GET or POST /licenses/check
Reports whether a key is usable. /licenses/validate is the same route under a second name.
- Parameters:
license_keyrequired.site_url,slugandproduct_idoptional. - Answer: HTTP 200 for any known key, with the license object and a
validfield.successmatchesvalid. - Codes:
valid,site_not_active,license_expired,license_revokedorlicense_inactive. - Errors:
missing_license(400),invalid_license(403).
valid is true when the status is active and, if you sent site_url, that site is activated. Without site_url, it only tells you the key is active.
curl "https://yourstore.com/wp-json/stackless/v1/licenses/check?license_key=GTP-ABCDE-FGHJK-MNPQR-STUVW&site_url=https://client-site.com&slug=my-plugin"
GET or POST /licenses/version
The update check. Its fields follow WordPress’s plugin update format.
- Parameters:
license_keyrequired, plusslugorproduct_id.site_urlandversion(the installed version) optional. - Fields:
slug,url(product page),new_version,tested,requires,requires_php,icons,banners,sections.changelog,package,download_link,licenseandupdate_available. - Errors:
missing_license(400),invalid_license(403, also when no slug or product ID is sent).
package and download_link hold the same signed link. They’re empty when the license isn’t active, or when you send a site_url that isn’t activated on the key. In that case you also get upgrade_notice with the reason. update_available is true when you didn’t send version or the store’s version is higher.
success stays true whenever the key is recognized, so a client always learns the latest version. code says whether this site may download it: valid, or site_not_active, license_expired, license_revoked or license_inactive when package is empty.
curl "https://yourstore.com/wp-json/stackless/v1/licenses/version?license_key=GTP-ABCDE-FGHJK-MNPQR-STUVW&slug=my-plugin&site_url=https://client-site.com&version=1.1.0"
GET or POST /licenses/info
The “View details” data, in WordPress’s plugin information format. It works without a key.
- Parameters:
slugrequired without a key. Withlicense_key, sendslugorproduct_idtoo.site_urloptional. - Fields:
name,slug,version,author(a link to your store),homepage,url,last_updated(the product’s last edit),tested,requires,requires_php,icons,banners,sections.description,sections.changeloganddownload_link. - Errors:
unknown_product(404) when no published or private product with licensing on uses that slug. With a key:invalid_license(403).
download_link is filled only when you send an active key and a site_url that’s activated on it, the same rule as version.
curl "https://yourstore.com/wp-json/stackless/v1/licenses/info?slug=my-plugin"
GET /licenses/download
Serves the update package. You don’t build this URL yourself: copy it from package or download_link.
- Parameters (all required):
license(license ID),product(product or variation ID),expires(Unix time) andsignature. - Success: a zip on your server is streamed as an attachment. A remote package URL gets a 302 redirect.
- Errors:
invalid_download(403) for a bad signature, an expired link or a product mismatch.license_expired,license_revokedorlicense_inactive(403) when the license stopped being usable after the link was made.no_package(404) when the product has no package or the file is missing.
The link is valid for Download link lifetime (hours), 24 by default, and changing any part of it breaks the signature. Local files must sit inside wp-content or the WordPress folder, and PHP files are never served.
Rate Limits
The settings live under License API and updates:
- Requests per minute (default 30, from 5 to 600): per IP address, and separately per license key.
- Failed lookups before a block (default 10, from 3 to 200): unknown keys, and bad download signatures, from one IP address within 15 minutes. After that, every request from that IP is refused until the window ends.
- The download route has its own per-IP limit: Requests per minute, but never fewer than 10.
A refused request gets HTTP 429 with code rate_limited, the message “Too many requests. Try again in a few minutes.”, a Retry-After header and data.retry_after in seconds.
The client IP comes from REMOTE_ADDR. Behind a proxy or CDN, return the real address with the stackless_software_licensing_client_ip filter. Forwarded headers aren’t read by default because they can be spoofed.
Error Codes and Messages
invalid_license: “This license key is not valid for this product.”missing_license: “A license key is required.”license_expired: “This license has expired. Renew it to keep receiving updates and support.”license_revoked: “This license was revoked.”license_inactive: “This license is inactive.”activation_limit_reached: “This license has reached its site limit. Deactivate another site or upgrade the license.”invalid_site: “The site address is missing or not valid.”site_not_active: “This site is not activated for this license.”activation_failed: “The site could not be activated. Try again.”unknown_product: “No product uses this software slug.”rate_limited: “Too many requests. Try again in a few minutes.”invalid_download: “This download link is invalid or has expired.”
Filters for the API
stackless_software_licensing_payload: change the license object.stackless_software_licensing_version_payload: change the version answer.stackless_software_licensing_info_payload: change the info answer.stackless_software_licensing_allow_activation: return aWP_Errorto refuse an activation.stackless_software_licensing_package_download(action): runs just before a package is served.stackless_software_licensing_site_activated(action): runs after a new site is activated.
Good to Know
- An unknown key, a key for another product and a malformed key all get the same
invalid_licenseanswer. That’s on purpose. - Every
invalid_licenseanswer counts as a failed lookup for the caller’s IP address, so a buggy integration that retries a wrong key can block itself for 15 minutes. - The
versionanswer includes only the changelog undersections. Theinfoanswer adds the product description.