LicenseKit documentation

Concepts

Product
The app you license. Its code, for example example-studio, is what the SDK sends as product.
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.

Lease claims
ClaimMeaning
issIssuer, licensekit
subLicense ID
audProduct code
jtiID of this lease
iat, nbfIssued at, not before (Unix seconds; 5 minutes of clock skew allowed)
expLicense expiry; absent for perpetual licenses
revCheck-in deadline
grcOffline grace in seconds
mntEnd of updates, if the policy tracks it
fpDevice fingerprint hash
featFeatures
cusCustomer 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

OptionMeaning
serverUrlLicense server URL
productProduct code
publicKeyOne 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.
storageDirWhere license.dat is kept
appVersionSent to the server on activation and check-in
secureStoreEncrypts license.dat; encrypt and decrypt may return promises; isProtected reports whether the storage is really protected
graceDaysUsed only when a lease carries no grc claim; default 7
requestTimeoutMsDefault 8000

Methods

activate(licenseKey, machineName?)
Takes a seat for this computer and stores the lease. Throws LicenseError with 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 valid and grace states that include the feature.

States

StatusMeaning
validThe lease is valid now.
graceThe check-in deadline has passed and the computer is offline; the app works until revalidateBy.
invalidThe license cannot be used; reason is a lease failure or the server's error code.
unlicensedNo license is stored on this computer.
clock_suspectThe 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": "..." }.

CodeHTTPMeaning
invalid_key404The key does not exist for this product.
license_suspended403The license is suspended.
license_revoked403The license is revoked.
license_expired403The license has expired.
activation_limit409All seats are in use; the body also has maxActivations.
not_activated404This computer holds no seat on the license.
validation_failed400The body is not valid.
rate_limited429Too many requests from this address.

Admin API

Send the admin key as x-admin-key or as Authorization: Bearer.

EndpointPurpose
GET /api/overviewCounts and recent events
GET /api/productsProducts
POST /api/productscode, name, optional defaultFeatures; also creates the default policy
GET /api/policiesPolicies; optional ?productId=
POST /api/policiesproductId, code, name, optional maxActivations, durationDays (null for perpetual), maintenanceDays, revalidateHours, graceDays, features
GET /api/licensesLicenses; optional ?q= (customer, email or key ending) and ?productId=
POST /api/licensesproductId, customerName, optional policyId, customerEmail, maxActivations, expiresAt (YYYY-MM-DD or null), features. Returns the key once.
GET /api/licenses/:idA license with its devices and history
PATCH /api/licenses/:idOptional maxActivations, expiresAt, features, status (active, suspended, revoked)
DELETE /api/activations/:idRemoves 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.