License states
At any time, the SDK sums up the license in one state. Your app only needs it to decide what to show: the app unlocked, the activation screen, or why it’s locked.
license.getState(); // "ACTIVE"
const unsubscribe = license.onStateChange((state) => {
for (const window of BrowserWindow.getAllWindows()) {
window.webContents.send("license:state", state);
}
});The states
| State | Unlock the app? | Meaning |
|---|---|---|
LOADING | Wait | The SDK is checking the stored key on startup. |
MISSING_KEY | No | No key is stored yet. |
ACTIVE | Yes | The license is valid: validated online, or with a stored token that hasn’t expired. |
INVALID_KEY | No | LicenseManager answered not_found: the key doesn’t exist in this product. |
EXPIRED | No | LicenseManager answered license_expired. |
VERSION_NOT_ALLOWED | No | LicenseManager answered version_not_allowed, or, offline, the stored token doesn’t include this version. |
MACHINE_LIMIT_REACHED | No | LicenseManager answered machine_limit_reached. |
OFFLINE_EXPIRED | No | LicenseManager can’t be reached, and there’s no valid stored token. |
CLOCK_TAMPERED | No | LicenseManager can’t be reached, and the system clock was moved. |
ACTIVE is the only state that unlocks your app. Whether it’s ACTIVE online or offline, see getOfflineState() in working offline.
LOADING
The state on construction, while the stored key is checked. With a valid stored token, it’s short: the SDK becomes ACTIVE once the clock check is done, usually under a second, up to 5 seconds offline, and validates online in the background. Without one, it lasts as long as the online validation: usually under a second, up to about 30 seconds on a network that doesn’t answer, with the retries. See startup. The SDK never goes back to LOADING afterwards.
Show a splash screen, or nothing, until ready() resolves.
MISSING_KEY
No key is stored: the app was never activated on this computer, or its stored data was deleted. Show the activation screen. The SDK makes no request in this state.
ACTIVE
The license is valid. Either the last validation succeeded, or LicenseManager couldn’t be reached and the stored token is valid: its signature, product, key and fingerprint match, it hasn’t expired, and its mv includes this version.
The SDK renews the token in the background. Nothing to do.
INVALID_KEY
The stored key doesn’t exist in this product anymore. Usually, you deleted the license, or the app was built for another product than the one the key belongs to, like a test key in a release build.
Show the activation screen, with: “Your license key is no longer valid. Enter a new one.”
EXPIRED
The license’s expiration date has passed. Offer to renew, with a link to your pricing page or the customer portal, and a I renewed my license button that calls refresh(): when you extend the license, the same key works again, and the customer has nothing to enter.
VERSION_NOT_ALLOWED
The license doesn’t include this version of your app, usually because its updates ended before it was released. getLatestAllowedVersion() returns:
| Value | When | What to show |
|---|---|---|
A version, like "2.3.1" | LicenseManager answered with the latest version the license allows. | “Your license includes Acme Photo up to 2.3.1.” Offer to download it, or to upgrade the license. |
null | LicenseManager answered: the license allows none of your releases. | Offer to upgrade the license. |
undefined | The refusal comes from the stored token, offline: it only knows the license’s max version, not your latest release. | “Your license doesn’t include this version. Connect to the internet to check for the versions it includes.” |
See versions offline.
MACHINE_LIMIT_REACHED
The license is active on as many machines as it allows, and this one isn’t one of them. It happens on a new computer, or after this computer was deactivated and the license’s places were taken in the meantime.
Show how to free a machine, with a link to the customer portal, and a Try again button that calls refresh().
OFFLINE_EXPIRED
LicenseManager can’t be reached, and there’s no stored token, or it has expired, or it fails verification. The customer stayed offline longer than the license’s max offline duration, or the token was lost.
Ask the customer to connect to the internet. The SDK retries on its own, every 5 minutes at first, then up to every hour, and becomes ACTIVE as soon as it succeeds. A Try again button calling refresh() saves them the wait.
CLOCK_TAMPERED
LicenseManager can’t be reached, and the clock check found the system clock was set back, or is more than 5 minutes off. The stored token isn’t trusted, even if it’s still valid.
Ask the customer to set their date and time automatically, and to connect to the internet. The SDK retries like for OFFLINE_EXPIRED.

How states change
The states fall into three groups:
| Group | States | Does the SDK retry on its own? |
|---|---|---|
| Unlocked | ACTIVE | Yes: it renews the token before it expires. |
| Refused by LicenseManager | INVALID_KEY, EXPIRED, VERSION_NOT_ALLOWED, MACHINE_LIMIT_REACHED | No. The stored token is deleted, and nothing is scheduled. The SDK validates again on refresh(), setLicenseKey(), or the next launch. |
| Offline | OFFLINE_EXPIRED, CLOCK_TAMPERED, and VERSION_NOT_ALLOWED when it comes from the stored token | Yes: with a backoff from 5 minutes to 1 hour, until LicenseManager answers. |
A refused license stays refused for the whole session: if you extend an expired license, the app notices it at its next launch, or when the customer clicks a button calling refresh(). That’s why the locked screens should always have one.
Listening to changes
onStateChange(listener) calls listener with the new state each time it changes, and returns a function that removes the listener.
- It’s not called with the current state when you subscribe: read
getState()for that. - It’s only called when the state changes: a renewal that keeps the app
ACTIVEdoesn’t call it. - It’s called synchronously, in the order listeners were added.
- An error thrown by a listener is swallowed: it doesn’t break the SDK, or stop the other listeners.
ready() returns the same promise every time, resolved with the state on startup: ACTIVE right away with a valid stored token, otherwise once the startup validation is done. It never rejects. Since the validation of a valid stored token runs after it, always keep a listener to lock the app.
Linking to the customer portal
getPortalUrl() returns the link to your product’s customer portal, like https://app.licensemanager.dev/portal/x7Kp2QaZ, where customers renew their license and free their machines. Open it in the customer’s browser from your locked screens:
import { ipcMain, shell } from "electron";
ipcMain.handle("license:open-portal", () => shell.openExternal(license.getPortalUrl()));To bring the customer back to your website after, add a return_url: see allowed return domains.
Edge cases
A refused license, launched offline. The stored token was deleted when the license was refused. Launched offline, the SDK can’t ask LicenseManager, and has no token to fall back on: the state is OFFLINE_EXPIRED, not EXPIRED. It becomes EXPIRED again once online.
An INVALID_KEY key stays stored. The SDK validates it again on each launch, and gets INVALID_KEY again, until the customer enters another key.
A machine deactivated from the dashboard. The app doesn’t notice until its next validation. At that validation, LicenseManager activates the machine again if the license has a free place, and the app stays ACTIVE. When the license is full, it answers machine_limit_reached, and the state becomes MACHINE_LIMIT_REACHED.
A license extended while the app runs EXPIRED. Nothing happens until refresh() or the next launch.
The organization is over its plan (plan_limit_exceeded), or LicenseManager answers 500. Handled like being offline: the app keeps running on its stored token, and the customer notices nothing while it’s valid.