Skip to content
Gaurav TiwariDocs

License API Reference

Version 1.0.0-beta.1 Updated October 28, 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-edd/v1 namespace:

https://yourstore.com/wp-json/stackless-edd/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, or more than 128, 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 download_id: optional on most routes. When you send them, they must match the key’s download, or you get invalid_license. product_id is read as an alias for download_id.
  • Caching: answers from activate, deactivate, check, version and info carry Cache-Control: no-store, private.

A download_id for another download still matches when both downloads use the same Software slug. That keeps a client working after a plan switch moves the key to another download of the same software.

Response Shape

A success has success, code and message, followed by the route’s fields. Here’s an activation:

{
  "success": true,
  "code": "activated",
  "message": "Site activated.",
  "status": "active",
  "download_id": 123,
  "price_id": null,
  "product_name": "Formwise Pro",
  "expires_at": "2027-10-30T23:59:59+00:00",
  "lifetime": false,
  "activation_limit": 5,
  "activations_count": 2,
  "activations_left": 3,
  "site": "client-site.com",
  "site_active": true,
  "is_local": false,
  "activation_id": 42
}

An error from a route has success: false, a code, a message, data.status with the HTTP status 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.",
  "data": { "status": 409 },
  "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.
  • download_id and price_id: the purchased download and price option (null for none).
  • product_name: the download name, with the price option name for variable pricing.
  • 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 download_id optional.
  • Success: code activated or already_active, the license object with site fields plus 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 (500).
curl -X POST https://yourstore.com/wp-json/stackless-edd/v1/licenses/activate \
  -d license_key=FWP-ABCDE-FGHJK-MNPQR-STUVW \
  -d site_url=https://client-site.com \
  -d slug=my-plugin

The key must be active to activate a site. The stackless_edd_software_licensing_allow_activation filter can refuse an activation by returning a WP_Error. Its code and message go back to the caller, with the status from its error data or 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 download_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-edd/v1/licenses/deactivate \
  -d license_key=FWP-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 download_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 -X POST https://yourstore.com/wp-json/stackless-edd/v1/licenses/check \
  -d license_key=FWP-ABCDE-FGHJK-MNPQR-STUVW \
  -d site_url=https://client-site.com \
  -d 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 download_id. site_url and version (the installed version) optional.
  • Fields: slug, url (the Homepage field or the download page), new_version, tested, requires, requires_php, icons, banners, sections.changelog, package, download_link, license and update_available. upgrade_notice appears when the download has one.
  • Errors: missing_license (400), invalid_license (403, also when no slug or download ID is sent, or the download was deleted).

package and download_link hold the same signed link. They’re empty unless the license is active and you send a site_url that’s activated on the key. When they’re empty, upgrade_notice carries the reason instead. 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.

curl -X POST https://yourstore.com/wp-json/stackless-edd/v1/licenses/version \
  -d license_key=FWP-ABCDE-FGHJK-MNPQR-STUVW \
  -d slug=my-plugin \
  -d site_url=https://client-site.com \
  -d 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 download_id too. site_url optional.
  • Fields: name, slug, version, author (a link), author_profile, homepage, url, last_updated (the download’s last edit, in GMT), tested, requires, requires_php, icons, banners, sections and download_link.
  • Sections: description and changelog always, installation and faq when those tabs are filled in.
  • Extra fields: requires_plugins when set, screenshot_url for a theme and upgrade_notice when set.
  • Errors: unknown_product (404). With a key: invalid_license (403).

Without a key, the route only describes published downloads without a password that have licensing on. A key for the download also opens private and password-protected ones. Drafts never answer.

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. With a key and an activated site, the answer also has versions: each version the license may download, mapped to a signed link.

curl -X POST https://yourstore.com/wp-json/stackless-edd/v1/licenses/info \
  -d slug=my-plugin

GET /licenses/download

Serves the update package. You don’t build this URL yourself: copy it from package, download_link or versions.

  • Parameters: license (license ID), download (download ID), expires (Unix time) and signature, all required. version is added for an older release.
  • Success: a zip on your server is streamed as a file. A remote package URL gets a 302 redirect.
  • Errors: invalid_download (403) for a bad signature, an expired link or a download mismatch. license_expired, license_revoked or license_inactive (403) when the license may no longer download it. unknown_version (404) when an older version is gone. no_package (404) when the download 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 on Stackless → Settings → Software Licensing:

Stackless → Settings → Software Licensing in Stackless for EDD, with the store's license API address and the Requests per minute, Failed lookups before a block and Download link lifetime (hours) settings
  • 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, that IP is refused on every route except download 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_edd_software_licensing_client_ip filter, or every customer shares one limit. Forwarded headers aren’t read by default because they can be spoofed.

Error Codes and Messages

CodeMessage
invalid_licenseThis license key is not valid for this download.
missing_licenseA license key is required.
license_expiredThis license has expired. Renew it to keep receiving updates and support.
license_revokedThis license was revoked.
license_inactiveThis license is inactive.
activation_limit_reachedThis license has reached its site limit. Deactivate another site or upgrade the license.
invalid_siteThe site address is missing or not valid.
site_not_activeThis site is not activated for this license.
activation_failedThe site could not be activated. Try again.
unknown_productNo download uses this software slug.
rate_limitedToo many requests. Try again in a few minutes.
invalid_downloadThis download link is invalid or has expired.
unknown_versionThis version is no longer available.
no_packageNo package is available for this download yet.

Filters for the API

  • stackless_edd_software_licensing_payload: change the license object. Receives the data and the license row.
  • stackless_edd_software_licensing_version_payload: change the version answer. Receives the data, the license row and the download ID.
  • stackless_edd_software_licensing_info_payload: change the info answer. Receives the data, the download ID and the license row (or null).
  • stackless_edd_software_licensing_allow_activation: return a WP_Error to refuse an activation. Receives true, the license row, the normalized site and whether it’s a development site.
  • stackless_edd_software_licensing_client_ip: return the real client address behind a proxy.
  • stackless_edd_software_licensing_package_download (action): runs just before a package is served.
  • stackless_edd_software_licensing_site_activated (action): runs after a new site is activated.

More hooks are listed in Hooks and Functions.

Good to Know

  • An unknown key, a key for another download 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 description and any Installation and FAQ tabs.
  • With plain permalinks, use https://yourstore.com/?rest_route=/stackless-edd/v1/licenses/{route}. The license client falls back to this address on its own.
Share

Was this helpful?

Still stuck? Open a support ticket