Validation and renewal
The SDK validates the key online on every launch, then again in the background before the token expires. Each validation that succeeds gives a new token, which replaces the stored one.
When the SDK validates
| When | Why |
|---|---|
| On launch, when a key is stored | Changes made on LicenseManager, like an expired license or a deactivated machine, apply right away when online. With a valid stored token, it runs in the background: see startup. |
When the customer activates a key, with setLicenseKey() | See activating a license. |
| At the renewal point, in the background | To get a new token before the current one expires. See below. |
| While offline, on a backoff | To get back online as soon as possible. See retries. |
When you call refresh() | Like after the customer renewed their license. |
Each validation counts as one API request toward your plan’s quota: with the default settings, a running app makes about one request per launch, plus one every 2/3 of the token’s lifetime. Retries only happen when LicenseManager can’t answer.
What’s sent
{
"key": "7KQ2M-X9ZPA-H4TRC-WN6EY-B3JDF",
"fingerprint": "q3M0…:Xk9f…:x7Kp2QaZ",
"version": "2.3.1",
"name": "Janes-MacBook-Pro.local",
"platform": "macOS"
}| Field | From |
|---|---|
key | The stored key. |
fingerprint | See machine fingerprint. |
version | The version option, or app.getVersion(). |
name | The machineName option, or the hostname. |
platform | The platform option, or macOS, Windows or Linux. |
Each request times out after 10 seconds. Transient failures are retried.
What’s checked
The token in the answer is verified before it’s trusted, exactly like the offline validation does:
- its signature, with your public key, accepting only
EdDSA, aud, equal to yourproductId,sub, equal to the key,fp, equal to this computer’s fingerprint,expandnbf, against the system clock,iatandexpare present.
A token that fails is an invalid_response error, and is never stored.
How the answer is handled
| Answer | During setLicenseKey() | During any other validation |
|---|---|---|
| A valid token | Stored, ACTIVE. | Stored, ACTIVE. |
not_found, license_expired, version_not_allowed, machine_limit_reached | Throws. Nothing changes. | The stored token is deleted, and the state becomes INVALID_KEY, EXPIRED, VERSION_NOT_ALLOWED or MACHINE_LIMIT_REACHED. No renewal is scheduled. |
plan_limit_exceeded, validation_error, internal_error, an unknown error | Throws. Nothing changes. | Falls back on the stored token: see working offline. |
| No answer, a timeout, an error answer that isn’t JSON, after the retries | Throws network_error. Nothing changes. | Falls back on the stored token. |
| A token that fails verification | Throws invalid_response. Nothing changes. | Falls back on the stored token. |
Only the four locking codes can lock an app that was working. Whatever goes wrong with the network or with LicenseManager, a customer with a valid stored token keeps working.
Startup
On launch, with a stored key, the SDK first checks the stored token, like offline:
- The stored token is valid: the state becomes
ACTIVEandready()resolves right away, without waiting for LicenseManager. The online validation runs in the background. When it refuses the license, the state changes, and youronStateChangelistener locks the app. - No valid stored token, like in
OFFLINE_EXPIRED,CLOCK_TAMPERED, orVERSION_NOT_ALLOWEDfrom the token:ready()waits for the online validation, retries included, up to about 30 seconds.
const state = await license.ready();
if (state === "ACTIVE") openMainWindow();
else openActivationWindow();
// The background validation may still lock the app.
license.onStateChange((state) => {
if (state !== "ACTIVE") lockApp(state);
});Retries
Each validation retries transient failures on its own, with an exponential backoff: each wait doubles, with some randomness, so that apps don’t all retry at the same time.
| Retried | Not retried |
|---|---|
No answer, a timeout, and the HTTP statuses 408, 429 and 5xx, like internal_error. | Every other answer: the four locking codes, plan_limit_exceeded, validation_error, and a token that fails verification. |
How long it retries depends on whether someone waits for the result:
| Validation | First wait | Longest wait | Gives up after |
|---|---|---|---|
setLicenseKey(), refresh(), and startup without a valid stored token | 0.5 second | 5 seconds | 30 seconds |
| In the background: startup with a valid stored token, renewals, and retries while offline | 2 seconds | 1 minute | 5 minutes |
A request never runs past the end of that time: the last one times out early if needed. Once it’s over, the validation fails with the last error: setLicenseKey() throws it, the other validations fall back on the stored token.
These retries happen within one validation. When it still fails, the SDK schedules the next one later, from 5 minutes to 1 hour: see working offline.
When the key is validated again
After each successful validation, the SDK schedules the next one at 2/3 of the token’s lifetime. With a 1 week token, the next validation is 4 days and 16 hours later: the customer can go offline right before it, and still has 2 days and 8 hours left.
The token’s lifetime is the license’s max offline duration, shortened when the license expires sooner.
minOfflineDurationMs
If your customers must be able to stay offline for a given time whenever they go offline, set minOfflineDurationMs: the SDK renews earlier, so that at least that much validity remains after the renewal point.
new LicenseManager({
productId: "x7Kp2QaZ",
publicKey: PUBLIC_KEY,
minOfflineDurationMs: 3 * 24 * 60 * 60 * 1000, // 3 days
});The renewal happens at the earliest of:
- 2/3 of the token’s lifetime,
- the token’s expiration minus
minOfflineDurationMs, but never less than 1 day after the token was issued.
The 1 day floor stops a minOfflineDurationMs close to or longer than the token’s lifetime from validating the key again and again. It only limits minOfflineDurationMs: tokens shorter than 1 day are still renewed at 2/3 of their lifetime.
| Token lifetime | minOfflineDurationMs | Renewed after | Left when renewed |
|---|---|---|---|
| 1 week | 0 (default) | 4 days 16 hours | 2 days 8 hours |
| 1 week | 3 days | 4 days | 3 days |
| 1 week | 1 week | 1 day (the floor) | 6 days |
| 4 weeks | 2 weeks | 2 weeks | 2 weeks |
| 1 hour | 0 | 40 minutes | 20 minutes |
| 1 hour | 1 day | 40 minutes | 20 minutes |
minOfflineDurationMs can’t give more offline time than the token’s lifetime: the max offline duration of the license is the limit. To let customers stay offline longer, raise the max offline duration of the policy.
Near the license’s expiration
A token never outlives its license: a license expiring in 2 days gives tokens that expire in 2 days. Each renewal happens at 2/3 of a shorter and shorter lifetime, so the SDK validates more often in the last day, a handful of requests in total. At the expiration, LicenseManager answers license_expired, and the state becomes EXPIRED. Offline at that moment, the token expires and the state becomes OFFLINE_EXPIRED.
When you extend the license before it expires, the next renewal gets a token with the new date.
Timers
The renewal uses a timer that doesn’t keep your app running: quitting isn’t delayed by it. Long delays are split into steps of at most 1 hour, each checking the system clock: after the computer sleeps, or when its clock changes, the validation runs at most about 1 hour late, not days late. A 3 month token is renewed after 2 months as expected.
To validate as soon as the computer wakes, call refresh() from Electron’s powerMonitor:
import { powerMonitor } from "electron";
powerMonitor.on("resume", () => void license.refresh());Each call is one API request: don’t do it on events that happen often.
refresh()
refresh() validates the stored key now, and resolves with the new state.
ipcMain.handle("license:refresh", () => license.refresh());- It waits for
ready()first, then validates, with the short retries: it resolves within about 30 seconds. - It never throws: the result is in the state, handled like any background validation. A network error falls back on the stored token.
- Without a stored key, it does nothing, and resolves with
MISSING_KEY. - On success, the next renewal is rescheduled from the new token.
- A call while another
refresh()orsetLicenseKey()of the same key is running shares its request. A background validation running at the same time doesn’t hold it up:refresh()sends its own request.
Use it for the buttons of locked screens, I renewed my license or Try again, and after events where the license may have changed.
Edge cases
The app is updated to a new version. The first launch of the new version validates with its version: when the license doesn’t include it, the state becomes VERSION_NOT_ALLOWED, with the latest version the license allows.
A long-running app. An app left open for weeks is validated by the renewals only, not on launch. Changes on LicenseManager reach it at the next renewal, at most 2/3 of the token’s lifetime later.
A license refused since the last launch. The stored token is still valid, so ready() resolves with ACTIVE, and the customer sees your app until the background validation comes back, usually under a second. The state then becomes EXPIRED, or another refused state: lock the app from your onStateChange listener.
Startup without a valid stored token, on a slow network. ready() waits for the validation, up to about 30 seconds with the retries. Show a splash screen meanwhile.
After dispose(). No renewal is scheduled anymore, pending retries stop, and the clock check stops. A request already running still completes, and can still change the state and call the listeners. refresh() and setLicenseKey() still work, but schedule nothing.