LicenseKit documentation
Concepts
- Product
- The app you license. Its code, for example
example-studio, is what the SDK sends asproduct. - Policy
- The defaults for new licenses of a product: seats, duration in days or perpetual, how long updates are included, how often the app checks in, how long it works offline after a missed check-in, and features. Every product has a default policy.
- License
- What a customer buys. It has a key, a policy, seats, an optional expiry, an optional end of updates, features and a status: active, suspended or revoked. Values given when the license is created override the policy.
- License key
- What the customer types, for example
LK-7Q2M-9XPA-K4TD-W8NE. It uses the Crockford Base32 alphabet, so I, L, O and U never appear; input ignores case, spaces and hyphens and reads I and L as 1 and O as 0. The server stores only a SHA-256 hash, so a key is shown once. - Device and seat
- Activating a license on a computer takes one seat. The SDK identifies the computer by a SHA-256 hash of the operating system's machine ID and the product code; the raw ID never leaves the computer. Activating again on the same computer reuses its seat.
- Lease
- The signed statement the server returns on every activation and check-in. The app stores it and verifies it offline.
- Check-in and offline grace
- Each lease has a check-in deadline set by the policy. After the deadline the app keeps working for the policy's offline grace period, but never past the license expiry. Then it must reach the server.
- Revocation window
- Suspending or revoking a license takes effect at the device's next check-in. A device that stays offline keeps working until its check-in deadline plus the grace period.
- Clock check
- The SDK remembers the latest time it saw. If the clock is more than a day earlier than that, or a lease looks issued in the future, the state is
clock_suspect.
The lease
A lease is a compact JSON Web Signature (RFC 7515) with the Ed25519 algorithm (RFC 9864). The header has alg, typ set to
licensekit-lease+jwt, and kid, the RFC 7638 thumbprint of the signing key. Verifiers accept only Ed25519, check the type, and choose the key by
kid.
| Claim | Meaning |
|---|---|
iss | Issuer, licensekit |
sub | License ID |
aud | Product code |
jti | ID of this lease |
iat, nbf | Issued at, not before (Unix seconds; 5 minutes of clock skew allowed) |
exp | License expiry; absent for perpetual licenses |
rev | Check-in deadline |
grc | Offline grace in seconds |
mnt | End of updates, if the policy tracks it |
fp | Device fingerprint hash |
feat | Features |
cus | Customer name |
Verification fails with one of these reasons, checked in this order:
malformed unsupported_algorithm wrong_type unknown_key bad_signature wrong_issuer
wrong_product wrong_device not_yet_valid expired revalidation_required
SDK
The SDK runs in the Electron main process or in plain Node.js 22.13 or later.
npm install @carchalabs/licensekit-electron
const kit = new LicenseKit({
serverUrl: 'https://licenses.example.com',
product: 'example-studio',
publicKey: [currentKeyPem, nextKeyPem],
storageDir: app.getPath('userData'),
appVersion: app.getVersion(),
secureStore: {
encrypt: (s) => safeStorage.encryptString(s),
decrypt: (b) => safeStorage.decryptString(b),
isProtected: () => process.platform !== 'linux' || safeStorage.getSelectedStorageBackend() !== 'basic_text',
},
});
Options
| Option | Meaning |
|---|---|
serverUrl | License server URL |
product | Product code |
publicKey | One PEM public key or a list; a lease is accepted if its kid matches one of them. An unusable key throws when the SDK is created. |
storageDir | Where license.dat is kept |
appVersion | Sent to the server on activation and check-in |
secureStore | Encrypts license.dat; encrypt and decrypt may return promises; isProtected reports whether the storage is really protected |
graceDays | Used only when a lease carries no grc claim; default 7 |
requestTimeoutMs | Default 8000 |
Methods
activate(licenseKey, machineName?)- Takes a seat for this computer and stores the lease. Throws
LicenseErrorwith the server's error code,network,clock_suspect, or a lease failure reason if the returned lease cannot be used. check()- Checks in with the server and stores the new lease. If the server cannot be reached, decides from the stored lease. Call it at startup and every few hours.
deactivate()- Frees this computer's seat and deletes the stored lease.
LicenseKit.hasFeature(state, feature)- True only for
validandgracestates that include the feature.
States
| Status | Meaning |
|---|---|
valid | The lease is valid now. |
grace | The check-in deadline has passed and the computer is offline; the app works until revalidateBy. |
invalid | The license cannot be used; reason is a lease failure or the server's error code. |
unlicensed | No license is stored on this computer. |
clock_suspect | The clock looks wrong; reason is clock_rollback or lease_from_future. |
Every state also has offline, storageProtected, features and expiresAt, and, when known, customer,
maintenanceUntil, revalidateBy, keyHint and a message for the user.
Quickstart
@carchalabs/licensekit-quickstart runs the SDK from a terminal against the public demo server, with a trial key from the
Try it section. It sends the device name LicenseKit quickstart and saves the lease unencrypted in the per-user application data folder.
npx @carchalabs/licensekit-quickstart activate LK-XXXX-XXXX-XXXX-XXXX
npx @carchalabs/licensekit-quickstart status
npx @carchalabs/licensekit-quickstart deactivate
The exit code is 0 when the license can be used (valid or grace), 1 when it cannot or a request failed, and 2 for a wrong command.
Client API
Called by the SDK and authenticated by the license key. Bodies are JSON; unknown fields are rejected. fingerprint is a 64-character lowercase SHA-256
hex digest. Each client address may call activate, validate and deactivate 30 times a minute; the key endpoints are not limited.
POST /v1/activate
Body: licenseKey, product, fingerprint, optional machineName, platform, appVersion.
Response: token (the lease), reused, license with id, customer, features, expiresAt, and revalidateBy.
POST /v1/validate
Body: licenseKey, product, fingerprint, optional appVersion. Response: valid, token, revalidateBy.
POST /v1/deactivate
Body: licenseKey, product, fingerprint. Response: deactivated: true.
GET /v1/keys
The verification keys as a JSON Web Key Set (RFC 7517): the current signing key first, then the next one if configured.
GET /v1/public-key
The current public key as PEM, for embedding in an app.
Errors
Every error has the body { "error": "...", "message": "..." }.
| Code | HTTP | Meaning |
|---|---|---|
invalid_key | 404 | The key does not exist for this product. |
license_suspended | 403 | The license is suspended. |
license_revoked | 403 | The license is revoked. |
license_expired | 403 | The license has expired. |
activation_limit | 409 | All seats are in use; the body also has maxActivations. |
not_activated | 404 | This computer holds no seat on the license. |
validation_failed | 400 | The body is not valid. |
rate_limited | 429 | Too many requests from this address. |
Admin API
Send the admin key as x-admin-key or as Authorization: Bearer.
| Endpoint | Purpose |
|---|---|
GET /api/overview | Counts and recent events |
GET /api/products | Products |
POST /api/products | code, name, optional defaultFeatures; also creates the default policy |
GET /api/policies | Policies; optional ?productId= |
POST /api/policies | productId, code, name, optional maxActivations, durationDays (null for perpetual), maintenanceDays, revalidateHours, graceDays, features |
GET /api/licenses | Licenses; optional ?q= (customer, email or key ending) and ?productId= |
POST /api/licenses | productId, customerName, optional policyId, customerEmail, maxActivations, expiresAt (YYYY-MM-DD or null), features. Returns the key once. |
GET /api/licenses/:id | A license with its devices and history |
PATCH /api/licenses/:id | Optional maxActivations, expiresAt, features, status (active, suspended, revoked) |
DELETE /api/activations/:id | Removes a device and frees its seat |
Order webhook
POST /api/webhooks/order
Creates a license from an order. Enabled when WEBHOOK_SECRET is set. The header X-Signature: sha256=<hex> must be the HMAC-SHA256 of
the raw body with that secret.
Body: orderId, productCode, customerName, optional customerEmail, seats, durationDays.
Response: duplicate: false, licenseId and licenseKey. The same orderId again answers duplicate: true
without a key.
Signing keys
The server signs with LICENSE_SIGNING_KEY. Apps embed its public half and the public half of the next key, which the server also publishes from
LICENSE_NEXT_PUBLIC_KEY or LICENSE_NEXT_PUBLIC_KEY_FILE. The next key's private half is kept offline. To rotate, the next key becomes the signing
key; installed apps already trust it. A new next key is then created for the following release. NIST SP 800-57 Part 1 suggests a cryptoperiod of about one to three
years for a private signature key.