Skip to content
Gaurav TiwariDocs

License API Reference

Updated October 17, 2026

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/ and example.com are the same site, while a subfolder install is its own site.
  • **slug and product_id:** optional on most routes. When you send them, they must match the key’s product, or you get invalid_license. product_id can 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, expired or revoked. A key past its expiry date reports expired straight away.
  • product_id and variation_id: the purchased product and variation (0 for none).
  • product_name: the product or variation name.
  • expires_at: ISO 8601 date in UTC, or null for lifetime.
  • lifetime: true when 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, or null when 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_key and site_url required. instance_id, slug and product_id optional.
  • Success: code activated or already_active, the license object with site fields, and activation_id.
  • Errors: missing_license (400), invalid_license (403), license_expired, license_revoked or license_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_key and site_url required. slug and product_id optional.
  • Success: code deactivated and 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_key required. site_url, slug and product_id optional.
  • Answer: HTTP 200 for any known key, with the license object and a valid field. success matches valid.
  • Codes: valid, site_not_active, license_expired, license_revoked or license_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_key required, plus slug or product_id. site_url and version (the installed version) optional.
  • Fields: slug, url (product page), new_version, tested, requires, requires_php, icons, banners, sections.changelog, package, download_link, license and update_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: slug required without a key. With license_key, send slug or product_id too. site_url optional.
  • 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.changelog and download_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) and signature.
  • 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_revoked or license_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 a WP_Error to 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_license answer. That’s on purpose.
  • Every invalid_license answer 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 version answer includes only the changelog under sections. The info answer adds the product description.
Share

Was this helpful?

Still stuck? Open a support ticket