Skip to main content

License calls: activate, validate, deactivate

Written for developers

This is the technical reference for the license calls your product makes. If you sell the product but don't write its code, read Not a developer? Start here and send this page to your developer.

New here? Start with the Quick start and the guide for your product type. This page is the reference for the three calls every product makes. SDK users (JavaScript, C++, C# / Unity) get these calls wrapped for them.

Looking for feature flags, offline tokens, in-app checkout, or rate limits? Those are on the advanced pages.

The three calls​

CallWhenWhat it does
ActivateThe customer clicks ActivateRegisters this device or website against the license.
ValidateRight after activating, at launch, dailySays whether the key is valid right now.
DeactivateThe customer clicks DeactivateFrees the device or website.

Gate on one field only: valid. true unlocks, false stays locked, for every reason (expired, refunded, unknown key). HTTP status is 200 even for a bad key. That is intentional, so nobody can probe which keys exist.

Only lock when the reply says valid: false. If there is no valid field at all (you are offline, rate limited, or the server had a hiccup), keep the previous state instead of locking a paying customer.

What you need from the admin panel​

  1. A product slug, for example resizer-pro. Sent on every call.
  2. A Client API key, shown once when created. pk_test_... for testing, pk_live_... for production.
  3. A license key to try (lic_...). Issue yourself one, no payment needed.
  4. The product client type: Native for desktop apps, plugins, games and WordPress (PHP runs on the server); Browser for code running inside a web page (browser extensions, web apps). See origins.

Concepts​

TermMeaning
ProductWhat you sell. Has a slug, an activation mode, and a client type.
PlanA price for that product, with a limit on devices or websites.
LicenseOne customer's right to use the product. The key looks like lic_....
ActivationOne device (seat) or website (domain) using a license. Counted against the plan limit.
API keypk_live_... / pk_test_.... Belongs to one product. Safe to embed in what you ship.

Activation mode is fixed when you create the product:

  • Seat: one activation per device or user (desktop, audio plugins, games).
  • Domain: one activation per website (WordPress, web apps). Licensr strips protocol, port and path.

Pick the mode by what your plan limit should count, not by where your code runs (that is the client type). A desktop app is Seat; a WordPress plugin is Domain even though it runs server-side. Domain needs a website hostname to identify each activation, so the product form only offers it for products that live on a website or server (choose Something else for those).

Base URLs and authentication​

EnvironmentAPI
Staginghttps://api.staging.licensr.app
Productionhttps://api.licensr.app

pk_test_... keys work on staging, pk_live_... keys on production.

Every call sends:

  • Authorization: Bearer <your API key>
  • Content-Type: application/json on POST

Browsers add an Origin header automatically. Native code does not, which is fine when the client type is Native.

Activate​

POST /v1/license/activate

{
"license_key": "lic_...",
"plugin_slug": "resizer-pro",
"activation_type": "seat",
"identifier": "stable-machine-id",
"label": "Studio iMac"
}
  • activation_type: seat or domain, matching the product.
  • identifier: the machine id (seat) or hostname (domain). See identifiers.
  • label: optional friendly name shown in the admin panel and customer portal.
{
"activation_id": "ad9b...",
"feature_flags": null,
"activation": {"type": "seat", "identifier": "stable-machine-id"}
}

The response contains an activation_id. Save it, you need it to deactivate. activation only repeats what you sent, so you can ignore it. Calling activate again with the same identifier returns the same activation_id and does not use another slot, so it is safe to call every launch.

Validate​

POST /v1/license/validate

{"license_key": "lic_...", "plugin_slug": "resizer-pro"}
{
"valid": true,
"status": "active",
"entitlement": "full",
"feature_flags": null,
"license": {
"plan_id": "1c4f...",
"activation_mode": "seat",
"max_seats": 3,
"max_domains": null,
"seats_in_use": 1,
"expires_at": "2027-01-15T00:00:00+00:00"
}
}

Only valid decides whether to unlock. The top level holds what your product acts on. Everything under license is information about the license that you can show the customer; never use it to decide access.

FieldWhat it is for
validThe answer. true unlocks, false stays locked.
statusChoosing a message: active, inactive (refunded or turned off), or expired.
entitlement, feature_flagsAdvanced. Ignore them for now.
license.expires_atShowing "renews on ..." in your license screen.
license.max_seats / seats_in_useShowing "1 of 3 devices used".

Validate does not use up a device slot. Call it at launch and about once a day in the background. That is how a refund, a cancellation, or an expiry reaches your product.

Deactivate​

POST /v1/license/deactivate

{"license_key": "lic_...", "activation_id": "ad9b..."}

Frees the slot. Wire it to a Deactivate button. Customers can also do it in their portal.

Errors​

{"detail": {"error": "<code>", "message": "<human readable>"}}
HTTPCodeMeaning
401missing_plugin_api_key / invalid_plugin_api_keyBad or missing API key.
403origin_not_allowedAllowed websites is not empty, and this website is not on it (or a Native app sends no website while the client type is Browser).
403origin_not_activatedDomain guard.
403plugin_mismatchThe API key belongs to a different product.
403license_inactiveLicense is not active.
403insufficient_scopeThe key cannot make this call, for example a Client key used for checkout.
403license_not_validToken call for a license that is not valid. No token is issued.
400activation_mode_mismatchseat vs domain does not match the product.
404license_not_found / activation_not_foundNothing with that key or id.
409activation_cap_exceededThe plan's device or website limit is reached.
422(list of field errors)Malformed body, for example a license key shorter than 8 characters.
429rate_limit_exceededToo many requests, see limits.

Identifiers​

The device limit only works if identifier is the same every time on one install and different on every machine.

  • Seat, recommended: a hash of the operating system's machine id plus your product slug (IOPlatformUUID on macOS, MachineGuid on Windows, /etc/machine-id on Linux). C++: licensr::compute_hardware_id. Unity: SystemInfo.deviceUniqueIdentifier. Where there is no machine id (sandboxes), generate a UUID once and store it. A reinstall then uses a new slot.
  • Domain: the hostname. shop.example.com and example.com are different.

Avoid MAC addresses, browser fingerprints, and the license key itself.

Typical flow​

When the customer enters a key

POST /activate with the machine id (save activation_id)
POST /validate
if valid -> unlock, store the key
if !valid -> show a message based on status, stay locked

At every launch and about once a day

POST /validate
if valid is false -> lock immediately
if no answer -> stay unlocked (optionally lock after a few days with no answer)

Moving to a new machine: the customer clicks Deactivate on the old one (or uses the portal), then activates on the new one.

Your product never talks to Stripe, PayPal, Mercado Pago, or Razorpay. Renewals, failed payments and refunds update the license, and your daily validate sees it.

cURL​

API_BASE=https://api.staging.licensr.app
PK=pk_test_...
LIC=lic_...

curl -s -X POST "$API_BASE/v1/license/activate" \
-H "Authorization: Bearer $PK" \
-H "Content-Type: application/json" \
-d "{\"license_key\":\"$LIC\",\"plugin_slug\":\"resizer-pro\",\"activation_type\":\"seat\",\"identifier\":\"stable-machine-id\",\"label\":\"Dev box\"}"

curl -s -X POST "$API_BASE/v1/license/validate" \
-H "Authorization: Bearer $PK" \
-H "Content-Type: application/json" \
-d "{\"license_key\":\"$LIC\",\"plugin_slug\":\"resizer-pro\"}"

Add -H "Origin: https://your-site.example" if your product is a Browser client type.

FAQ​

An unknown key looks inactive? Intentional. Licensr does not confirm whether a key exists.

Can I cache validate? Yes, locally, for a few minutes. Do not share the cache between installs.

The limit is reached but the seat count shows 0? Deactivated activations do not count. If a license is not used at all for 30 days, its devices or websites are released automatically so the customer can activate again.

Desktop app gets 403 origin_not_allowed? This only happens when Allowed websites is not empty. The product's client type is still Browser. Switch it to Native.

One key for two products? No. Sell a bundle as separate licenses.

Does the organization change the URLs? No. The API key identifies you. Only storefront URLs contain your slug.

Versioning: v1. New fields are additive. Removing fields or changing error codes waits for v2.