License calls: activate, validate, deactivate
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
| Call | When | What it does |
|---|---|---|
| Activate | The customer clicks Activate | Registers this device or website against the license. |
| Validate | Right after activating, at launch, daily | Says whether the key is valid right now. |
| Deactivate | The customer clicks Deactivate | Frees 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
- A product slug, for example
resizer-pro. Sent on every call. - A Client API key, shown once when created.
pk_test_...for testing,pk_live_...for production. - A license key to try (
lic_...). Issue yourself one, no payment needed. - 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
| Term | Meaning |
|---|---|
| Product | What you sell. Has a slug, an activation mode, and a client type. |
| Plan | A price for that product, with a limit on devices or websites. |
| License | One customer's right to use the product. The key looks like lic_.... |
| Activation | One device (seat) or website (domain) using a license. Counted against the plan limit. |
| API key | pk_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
| Environment | API |
|---|---|
| Staging | https://api.staging.licensr.app |
| Production | https://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/jsonon 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:seatordomain, 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.
| Field | What it is for |
|---|---|
valid | The answer. true unlocks, false stays locked. |
status | Choosing a message: active, inactive (refunded or turned off), or expired. |
entitlement, feature_flags | Advanced. Ignore them for now. |
license.expires_at | Showing "renews on ..." in your license screen. |
license.max_seats / seats_in_use | Showing "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>"}}
| HTTP | Code | Meaning |
|---|---|---|
| 401 | missing_plugin_api_key / invalid_plugin_api_key | Bad or missing API key. |
| 403 | origin_not_allowed | Allowed websites is not empty, and this website is not on it (or a Native app sends no website while the client type is Browser). |
| 403 | origin_not_activated | Domain guard. |
| 403 | plugin_mismatch | The API key belongs to a different product. |
| 403 | license_inactive | License is not active. |
| 403 | insufficient_scope | The key cannot make this call, for example a Client key used for checkout. |
| 403 | license_not_valid | Token call for a license that is not valid. No token is issued. |
| 400 | activation_mode_mismatch | seat vs domain does not match the product. |
| 404 | license_not_found / activation_not_found | Nothing with that key or id. |
| 409 | activation_cap_exceeded | The plan's device or website limit is reached. |
| 422 | (list of field errors) | Malformed body, for example a license key shorter than 8 characters. |
| 429 | rate_limit_exceeded | Too 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 (
IOPlatformUUIDon macOS,MachineGuidon Windows,/etc/machine-idon 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.comandexample.comare 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.