License Verification
Detailed guide on implementing license verification in your applications, including caching strategies, offline support, and best practices.
Your API address
The examples use https://your-license-api.com as a stand-in for your License Server's API address. Put yours in its place:
- Hosted account:
https://license-cloud-api.casazium.com. The console's Settings → API access → API base URL shows it with/v1already on the end,https://license-cloud-api.casazium.com/v1, so that value replaceshttps://your-license-api.com/v1as a whole: don't add a second/v1. Notlicense-cloud.casazium.com: that's the console, and an API call sent there gets a JSON 404 naming the right address. Where an example sends$ADMIN_API_KEY, use your account's API key from the same Settings → API access section instead: a hosted account doesn't acceptADMIN_API_KEY. - Self-hosted: your own server's address. The self-hosted bundle answers on
http://localhost:3001on the machine running it, sohttp://localhost:3001/v1replaceshttps://your-license-api.com/v1.
With the SDK
@casazium/license-sdk makes these calls for a JavaScript app. Setting it up is in Using the SDK; it's an ES module, so run these snippets as one (a .mjs file, or "type": "module" in your package.json).
import { CasaziumLicenseClient } from '@casazium/license-sdk';
const client = new CasaziumLicenseClient({
baseUrl: 'https://your-license-api.com/v1',
// The SDK sets no timeout of its own. Without one, a server that
// never answers holds up your app's start.
fetch: (url, init) => fetch(url, { ...init, signal: AbortSignal.timeout(5000) }),
});
const license = await client.verifyKey(key, 'your-product-uuid');
console.log(license.tier, license.limits, license.expires_at);verifyKey(key, expectedProductUuid) sends verify-license and resolves the response shown under Simple Verification. The second argument is required: your product's Product UUID, which the console shows on any of the product's licenses (see The license's page). Hard-code it in your app; it never changes.
verifyKey() throws rather than answering valid: false. It throws when the server refuses the key (unknown, revoked or expired), and when the key belongs to another product. So reaching the next line means licensed. What matters is whether the server said no or didn't answer: the error has the HTTP status on err.status (SDK 0.4.0 and later), and a network failure or timeout throws fetch's own error, with no status.
// The server said no: a 4xx other than 429, or verifyKey's own refusal
// of a license for another product (which has no status).
function isRefusal(err) {
if (err.status === undefined) return /product_uuid mismatch/.test(err.message);
// A 404 ending ": Not Found" is a route the server doesn't have (a server
// older than the SDK needs, or a base URL missing /v1), not a refused key.
if (err.status === 404 && /: Not Found$/.test(err.message)) return false;
return err.status >= 400 && err.status < 500 && err.status !== 429;
}
try {
await client.verifyKey(key, productUuid);
// Licensed: record the time of this good check.
} catch (err) {
if (isRefusal(err)) {
// Revoked, expired, unknown, or another product's: treat the license
// as invalid, and drop any offline file you keep for it.
} else {
// No answer: unreachable, timed out, rate limited (429), a server
// error, or a route the server doesn't have. Keep running on the last
// good check, within your own limit.
}
}Reading the answer explains the three outcomes. The SDK retries a network error or a 5xx up to retries times (default 3) before it throws; pass retries: 1 in the constructor for a faster start.
Two more checks the SDK makes:
- Is this machine still activated?
verifyKey()checks the license, not this machine, so it keeps passing after this machine's seat was freed. At launch, also callclient.validate(key, instanceId): see License Activation. - No network at all?
exportOfflineLicense()andverifySignedFile()check a signed license file with only your public key: see Cryptographic Offline Verification.
Without the SDK (curl or any language)
The rest of this section is the API the SDK calls, for an app in another language, or JavaScript without the SDK. Client Examples has complete Python, Java and Go clients.
Basic License Verification
Simple Verification
The most basic license verification checks if a key is valid:
curl -X POST https://your-license-api.com/v1/verify-license \
-H "Content-Type: application/json" \
-d '{
"key": "7yChyZcfMG23Dx1sjBoLziPFrH4n-6f28"
}'Response:
{
"valid": true,
"tier": "pro",
"product_id": "your-product",
"product_uuid": "b3f1c9a0-1234-4abc-9def-e2d4f5a6b7c8",
"limits": {
"users": 100,
"api_calls_per_day": 10000
},
"expires_at": "2025-12-31T23:59:59.000Z",
"issued_at": "2025-01-01T00:00:00.000Z",
"status": "active"
}expires_at is ISO 8601 (or null for a perpetual license), and so is issued_at from License Server 1.16.0. Before 1.16.0, issued_at was YYYY-MM-DD HH:MM:SS in UTC with no zone marker, which JavaScript reads as local time. To handle both, parse it with:
const parseTime = (value) =>
value == null
? null
: new Date(/[zZ]|[+-]\d\d:?\d\d$/.test(value) ? value : value.replace(' ', 'T') + 'Z');This adds the Z only when the value has no zone, so it also accepts ISO 8601 ending in Z or a ±HH:MM/±HHMM offset as it is, and returns null for a missing value (see the API Reference).
product_uuid is the license's real, immutable product identity - see Product IDs are per-tenant, not global. Compare it with your own product's UUID on every check: a valid key for a different product also comes back 200 {"valid": true}. If you're using @casazium/license-sdk, its verifyKey() requires this value and checks it for you.
Reading the answer
Every example on this page sorts a response into one of three outcomes:
| Outcome | Response | What to do |
|---|---|---|
| Licensed | 200, valid: true, and product_uuid matches yours | Run, and record the time of this good check |
| Refused | Any 4xx except 429 (404 unknown key, 403 revoked or expired), or a valid key for another product | Believe it: drop cached results and offline files, clear the last-good time |
| No answer | 429, 5xx, a network error or a timeout | Not a verdict on the key. Keep the last known state, but only up to a maximum time offline measured from the last good check |
Never report a no-answer as valid: false. Your users will see a license error during your outage, and a revoked key will look the same as a network hiccup. 429 responses carry a Retry-After header that can be many minutes; schedule the next check instead of waiting inline.
Verification with Instance Tracking
validate-license answers a different question: is this instance still activated? It returns 200 {"valid": true} or 200 {"valid": false}, with no reason, and doesn't include product_uuid. Use it alongside verify-license, not instead of it (see License Activation).
curl -X POST https://your-license-api.com/v1/validate-license \
-H "Content-Type: application/json" \
-d '{
"key": "7yChyZcfMG23Dx1sjBoLziPFrH4n-6f28",
"instance_id": "server-001"
}'Implementation Patterns
1. Startup Verification
Verify the license when your application starts. checkKey() makes one verify-license call and sorts the response into the three outcomes above; the later examples on this page reuse it.
// One verify-license call, sorted into licensed / refused / no-answer.
async function checkKey(apiUrl, key, expectedProductUuid) {
let response;
try {
response = await fetch(`${apiUrl}/verify-license`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key }),
signal: AbortSignal.timeout(10000)
});
} catch (error) {
return { outcome: 'no-answer', error: error.message }; // network error or timeout
}
if (response.status === 429 || response.status >= 500) {
return { outcome: 'no-answer', error: `HTTP ${response.status}` };
}
const body = await response.json().catch(() => null);
if (!response.ok) {
return { outcome: 'refused', error: body?.error || `HTTP ${response.status}` };
}
if (body?.valid !== true) {
return { outcome: 'no-answer', error: 'Unreadable response' };
}
// product_id strings aren't unique; product_uuid is.
if (body.product_uuid !== expectedProductUuid) {
return { outcome: 'refused', error: 'License is for a different product' };
}
return { outcome: 'licensed', license: body };
}
class ApplicationBootstrap {
constructor(licenseKey, apiUrl, productUuid) {
this.licenseKey = licenseKey;
this.apiUrl = apiUrl;
this.productUuid = productUuid; // your product's product_uuid, hard-coded
this.licenseInfo = null;
}
async initialize() {
const result = await checkKey(this.apiUrl, this.licenseKey, this.productUuid);
if (result.outcome === 'refused') {
this.showLicenseError(`License refused: ${result.error}`);
return false;
}
if (result.outcome === 'no-answer') {
// Not a verdict on the key, so don't tell the user it's invalid.
// To start without the server, see the caching and offline sections.
this.showLicenseError(`Can't reach the license server (${result.error}). Try again shortly.`);
return false;
}
this.licenseInfo = result.license;
// verify-license deliberately omits issued_to (a privacy choice). Use
// GET /v1/admin/license/:key (admin-authenticated) if you need it.
console.log(`License tier: ${result.license.tier}`);
this.startApplication(result.license);
return true;
}
startApplication(license) {
const limits = license.limits || {};
if (limits.users !== undefined) this.setMaxUsers(limits.users);
if (limits.api_calls_per_day !== undefined) this.setAPICallLimit(limits.api_calls_per_day);
console.log('Application started successfully');
}
// Placeholders - replace with your application's own.
setMaxUsers(max) {}
setAPICallLimit(max) {}
showLicenseError(message) { console.error(message); }
}
// Usage
const app = new ApplicationBootstrap(
process.env.LICENSE_KEY,
'https://your-license-api.com/v1',
'b3f1c9a0-1234-4abc-9def-e2d4f5a6b7c8'
);
app.initialize();2. Periodic Verification
Keep checking while the application runs, after the startup check above. A refusal stops the application at the next check; no answer keeps it running until maxOfflineHours have passed since the last good check (or since start(), if there hasn't been one yet).
class PeriodicLicenseChecker {
constructor(licenseKey, apiUrl, productUuid, { intervalMinutes = 60, maxOfflineHours = 24 } = {}) {
this.licenseKey = licenseKey;
this.apiUrl = apiUrl;
this.productUuid = productUuid;
this.interval = intervalMinutes * 60 * 1000;
this.maxOffline = maxOfflineHours * 60 * 60 * 1000;
this.intervalId = null;
this.lastGoodCheck = null; // time of the last licensed answer
}
start() {
if (this.intervalId) return;
this.startedAt = Date.now();
this.checkLicense();
this.intervalId = setInterval(() => this.checkLicense(), this.interval);
}
stop() {
clearInterval(this.intervalId);
this.intervalId = null;
}
async checkLicense() {
const result = await checkKey(this.apiUrl, this.licenseKey, this.productUuid);
if (result.outcome === 'licensed') {
this.lastGoodCheck = Date.now();
this.warnIfExpiringSoon(result.license);
} else if (result.outcome === 'refused') {
this.lastGoodCheck = null;
this.handleRefusedLicense(result.error);
} else if (Date.now() - (this.lastGoodCheck ?? this.startedAt) < this.maxOffline) {
console.warn(`No answer from the license server (${result.error}); trying again at the next check.`);
} else {
console.error(`No answer from the license server for ${this.maxOffline / 3600000} hours; shutting down.`);
this.initiateGracefulShutdown();
}
}
warnIfExpiringSoon(license) {
if (license.expires_at === null) return; // perpetual
const daysUntilExpiry = (Date.parse(license.expires_at) - Date.now()) / (1000 * 60 * 60 * 24);
if (daysUntilExpiry <= 30) {
console.warn(`License expires in ${Math.floor(daysUntilExpiry)} days`);
}
}
handleRefusedLicense(error) {
// The API has no `expired`/`revoked` fields; an invalid license is
// {valid: false, error: "<message>"}. "License is not active" covers
// revoked and any other non-active status, so it can't tell those apart.
if (error.includes('expired')) {
console.error('License has expired - please renew');
} else if (error.includes('not active')) {
console.error('License has been revoked or suspended');
} else {
console.error(`License refused: ${error}`);
}
this.initiateGracefulShutdown();
}
initiateGracefulShutdown() {
this.stop();
// Replace with your application's own shutdown.
process.exit(1);
}
}
// Usage
const licenseChecker = new PeriodicLicenseChecker(
process.env.LICENSE_KEY,
'https://your-license-api.com/v1',
'b3f1c9a0-1234-4abc-9def-e2d4f5a6b7c8'
);
licenseChecker.start();
process.on('SIGTERM', () => licenseChecker.stop());Caching Strategies
A cache saves calls and carries you through an outage. It must follow the same rule as everything else: a refusal empties it, and a cached result is only used for so long without a good check.
1. In-Memory Caching
// True once a license's own expires_at has passed (null = perpetual).
function licenseExpired(license) {
return license.expires_at !== null && !(Date.parse(license.expires_at) > Date.now());
}
class CachedLicenseVerifier {
constructor(apiUrl, productUuid, { cacheMinutes = 30, maxOfflineHours = 24 } = {}) {
this.apiUrl = apiUrl;
this.productUuid = productUuid;
this.cache = new Map(); // key -> { license, checkedAt }
this.cacheTTL = cacheMinutes * 60 * 1000;
this.maxOffline = maxOfflineHours * 60 * 60 * 1000;
}
// Resolves to checkKey()'s { outcome, license?, error? }, plus
// fromCache: true when the answer came from the cache.
async verifyLicense(key) {
const cached = this.cache.get(key);
const age = cached ? Date.now() - cached.checkedAt : Infinity;
const usable = cached && !licenseExpired(cached.license);
if (usable && age < this.cacheTTL) {
return { outcome: 'licensed', license: cached.license, fromCache: true };
}
const result = await checkKey(this.apiUrl, key, this.productUuid);
if (result.outcome === 'licensed') {
this.cache.set(key, { license: result.license, checkedAt: Date.now() });
return result;
}
if (result.outcome === 'refused') {
this.cache.delete(key); // believe the refusal
return result;
}
// No answer: the last good result stands, within the offline limit.
if (usable && age < this.maxOffline) {
return { outcome: 'licensed', license: cached.license, fromCache: true };
}
return result;
}
clearCache(key = null) {
if (key) this.cache.delete(key);
else this.cache.clear();
}
}2. Persistent Caching
Store the last good result on disk so a restart during an outage still works. This is the same logic as above, with the cache in a file.
A local cache isn't tamper-proof
Anyone who can write to the cache file can change checkedAt or the cached license and run indefinitely without the server. That's fine for riding out outages for honest users; it isn't a way to trust a license offline. For that, use the signed export in Cryptographic Offline Verification below.
const fs = require('fs').promises;
const path = require('path');
const crypto = require('crypto');
class PersistentLicenseCache {
constructor(cacheDir = './license-cache') {
this.cacheDir = cacheDir;
}
filePath(key) {
const keyHash = crypto.createHash('sha256').update(key).digest('hex').substring(0, 16);
return path.join(this.cacheDir, `license-${keyHash}.json`);
}
// { license, checkedAt } or null. Doesn't delete old entries: the
// caller decides what an entry's age allows.
async get(key) {
try {
return JSON.parse(await fs.readFile(this.filePath(key), 'utf8'));
} catch {
return null; // missing or unreadable
}
}
async set(key, license) {
try {
await fs.mkdir(this.cacheDir, { recursive: true });
await fs.writeFile(this.filePath(key), JSON.stringify({ license, checkedAt: Date.now() }));
} catch (error) {
console.error('Failed to cache verification:', error.message);
}
}
async remove(key) {
await fs.unlink(this.filePath(key)).catch(() => {});
}
}
// Uses checkKey() from Startup Verification and licenseExpired() from above.
class RobustLicenseVerifier {
constructor(apiUrl, productUuid, { cacheMinutes = 60, maxOfflineHours = 72, cacheDir } = {}) {
this.apiUrl = apiUrl;
this.productUuid = productUuid;
this.cacheTTL = cacheMinutes * 60 * 1000;
this.maxOffline = maxOfflineHours * 60 * 60 * 1000;
this.cache = new PersistentLicenseCache(cacheDir);
}
async verifyLicense(key) {
const cached = await this.cache.get(key);
const age = cached ? Date.now() - cached.checkedAt : Infinity;
const usable = cached && !licenseExpired(cached.license);
if (usable && age < this.cacheTTL) {
return { outcome: 'licensed', license: cached.license, fromCache: true };
}
const result = await checkKey(this.apiUrl, key, this.productUuid);
if (result.outcome === 'licensed') {
await this.cache.set(key, result.license);
return result;
}
if (result.outcome === 'refused') {
await this.cache.remove(key);
return result;
}
if (usable && age < this.maxOffline) {
console.warn(`License server unreachable (${result.error}); using the result cached ${Math.round(age / 60000)} minutes ago`);
return { outcome: 'licensed', license: cached.license, fromCache: true };
}
return result;
}
}Cryptographic Offline Verification
The caching strategies above are about resilience — surviving a temporary network outage using a previous verification result. This section covers something different: a license file that can be cryptographically verified with no server contact at all, not even once the network is back, using RSA public-key signatures.
This is distinct from the plain export, POST /v1/license-export (HMAC, shared secret), and POST /v1/verify-license-file / verify-license-file-base64 (which still check the database for revocation on every call). The offline-RSA path trades that live revocation check for true air-gapped verification.
With the SDK, fetch the file while online and check it later with only the public key:
import { CasaziumLicenseClient } from '@casazium/license-sdk';
// Online, once: fetch the public key (or embed it in your build) and the
// signed file, and store both.
const online = new CasaziumLicenseClient({ baseUrl: 'https://your-license-api.com/v1' });
const publicKey = await online.getPublicKey();
const file = await online.exportOfflineLicense(key); // { license, signature }
// At any time, with no network: true only for an authentic, unexpired
// license for your product.
const offline = new CasaziumLicenseClient({ publicKey });
const valid = await offline.verifySignedFile(file, 'your-product-uuid');The rest of this section shows the same steps without the SDK, then the two rules for keeping a stored file honest.
Fetching the Public Key
Fetch this once and embed or cache it in your application — it isn't meant to be re-fetched before every verification:
curl https://your-license-api.com/v1/public-key{ "publicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n" }Fetching a Signed License
curl -X POST https://your-license-api.com/v1/license-export/offline \
-H "Content-Type: application/json" \
-d '{"key": "7yChyZcfMG23Dx1sjBoLziPFrH4n-6f28"}'From License Server 1.21.0 the key goes in the request body, so proxies and server logs don't record it. Servers before 1.21.0 have only GET /v1/export-license/<key>/offline, now deprecated: see Export a license: key in the body. The SDK's exportOfflineLicense() above uses the POST from SDK 0.5.0, which needs License Server 1.21.0 or later. SDK 0.4.x and earlier use the GET, which keeps working.
{
"license": {
"type": "license",
"key": "7yChyZcfMG23Dx1sjBoLziPFrH4n-6f28",
"tier": "pro",
"product_id": "my-product",
"product_uuid": "b3f1c9a0-1234-4abc-9def-e2d4f5a6b7c8",
"expires_at": "2027-12-31T23:59:59.000Z",
"limits": {},
"issued_at": "2025-01-01T00:00:00.000Z",
"exported_at": "2026-06-01T08:00:03.512Z",
"livemode": true,
"paid_through": "2027-12-24T23:59:59.000Z"
},
"signature": "base64-encoded-rsa-signature"
}The last four fields are new in License Server 1.20.0: see What the file signs.
Verifying Fully Offline
import { createVerify } from 'node:crypto';
function verifyOfflineLicense(license, signature, publicKey, expectedProductUuid) {
const verifier = createVerify('SHA256');
verifier.update(JSON.stringify(license));
verifier.end();
const authentic = verifier.verify(publicKey, signature, 'base64');
// A signature only proves the bytes weren't tampered with. Three more
// checks, all of which @casazium/license-sdk's verifySignedFile() makes
// for you:
// - it's a license: release manifests are signed with the same key;
// - it's for *your* product (see "Product IDs are per-tenant, not
// global" in the API reference);
// - it hasn't expired: null is perpetual, and an unparseable date
// parses as NaN, so it's refused rather than read as perpetual.
const expiry = license.expires_at === null ? Infinity : Date.parse(license.expires_at);
return (
authentic &&
license.type === 'license' &&
license.product_uuid === expectedProductUuid &&
expiry > Date.now()
);
}With the SDK (0.3.0+), the same in one call — it resolves to false on anything else, including a malformed file:
const valid = await client.verifySignedFile({ license, signature }, expectedProductUuid);A file exported before License Server 1.10.0 has no type and is refused: export it again.
Verify the license object exactly as received. Sign-check JSON.stringify of the parsed object, in the order the response gave it. Don't rebuild it from the fields you know, sort its keys, or round-trip it through a typed struct or class: later releases add fields after limits, and a rebuilt object fails verification. Treat every field after limits as optional. The SDK already does this, and so does the code above, because it passes license straight through.
What the file signs
From License Server 1.20.0, the file also signs these fields after limits, in this order. Older servers send none of them, so treat a missing one as "not known":
| Field | What it is |
|---|---|
issued_at | When the license was issued. |
exported_at | The server's clock when it signed the file. |
livemode | true for a license from a live storefront purchase, false for a sandbox (test-mode) one. Left out when unknown, for example for a license you issued by hand. |
paid_through | The end of the latest period the buyer paid for with money. Left out when there is none. A free trial or a 100% coupon doesn't extend it, and a refund takes it back. |
Because they're signed, your app can trust them once the signature checks out, with no server call:
- Limit offline use by
exported_at. Count time offline from the file'sexported_atrather than from a timestamp your app saved itself, which a user can edit. If the computer's clock reads well beforeexported_at(allow some clock difference, say a day), the clock was set back. - Reject sandbox licenses. A file with
livemode: falsecame from a test-mode purchase. Your app can refuse it, even for files exported before you refused sandbox licenses on the server. - Keep what was paid for.
paid_throughis the date to base a "keep what you paid for" rule on, for example letting a buyer keep the builds released before it after a subscription ends.
With the SDK, read them from file.license once verifySignedFile() resolves true:
if (await client.verifySignedFile(file, 'your-product-uuid')) {
const { exported_at, livemode, paid_through } = file.license;
if (livemode === false) {
// a sandbox (test-mode) purchase: refuse it
}
// count time offline from exported_at, not from the local clock alone
}This confirms the license payload hasn't been tampered with since it was exported and hasn't expired, using only the cached public key — no server round-trip. What it does not know is whether the license has been revoked since export: a file for a perpetual license verifies forever. If live revocation status matters for your product, use verify-license-file/verify-license-file-base64 instead, or re-fetch the offline export when you can reach the server, with two rules:
- Drop the file when the server refuses the key. A revoked or expired key gets
403from the export route (404for an unknown key). If you keep the old file after that, a revoked user stays licensed. - Cap the time offline. Count from the last successful re-fetch, and stop accepting the stored file once that's too long ago. Without a cap, a user can stay offline on purpose and never see a revocation. With a file from 1.20.0 or later, count from its signed
exported_at.
429, 5xx, a network error, or a 404 ending : Not Found (a server older than 1.21.0, under SDK 0.5.0 and later) on the re-fetch isn't a refusal: keep the stored file. With the SDK (0.4.0+, which puts the HTTP status on err.status):
// `store` is wherever you keep { file, lastOnlineCheck } between runs.
async function checkOfflineLicense(client, key, productUuid, store, maxOfflineDays = 30) {
try {
const file = await client.exportOfflineLicense(key); // throws on any non-2xx
if (!(await client.verifySignedFile(file, productUuid))) {
await store.clear(); // not a license for this product
return false;
}
await store.save({ file, lastOnlineCheck: Date.now() });
return true;
} catch (error) {
// A 404 ending ": Not Found" is a route the server doesn't have (SDK
// 0.5.0+ against a server older than 1.21.0), not a refused key.
const routeMissing = error.status === 404 && /: Not Found$/.test(error.message);
const refused = !routeMissing &&
error.status >= 400 && error.status < 500 && error.status !== 429;
if (refused) {
await store.clear(); // revoked, expired or unknown: believe it
return false;
}
// No answer: fall through to the stored file.
}
const saved = await store.load();
if (!saved) return false;
// From License Server 1.20.0 the file signs exported_at; older files
// don't, so fall back to the time saved locally.
const exportedAt = Date.parse(saved.file.license.exported_at);
const since = Number.isNaN(exportedAt) ? saved.lastOnlineCheck : exportedAt;
const SKEW_MS = 24 * 60 * 60 * 1000; // allow for a clock a little behind
if (Date.now() + SKEW_MS < since) return false; // the clock was set back
const offlineFor = Date.now() - since;
return offlineFor < maxOfflineDays * 24 * 60 * 60 * 1000 &&
(await client.verifySignedFile(saved.file, productUuid));
}lastOnlineCheck lives on the user's machine, so it can be edited; the signed exported_at and expires_at can't be. The computer's clock can still be wound back to make the time offline look shorter, but not to well before exported_at. For a machine that is never online, nothing can see a revocation: issue those licenses with an expiry and renew them.
Feature Gating Based on License
License-Based Feature Control
class FeatureManager {
constructor(licenseInfo) {
this.licenseInfo = licenseInfo;
this.features = new Map();
this.initializeFeatures();
}
initializeFeatures() {
const tier = this.licenseInfo.tier;
const limits = this.licenseInfo.limits || {};
// Define features based on license tier
this.features.set('basic_features', true);
this.features.set('advanced_features', ['pro', 'enterprise'].includes(tier));
this.features.set('premium_features', tier === 'enterprise');
this.features.set('api_access', true);
this.features.set('bulk_operations', tier !== 'basic');
this.features.set('custom_integrations', tier === 'enterprise');
// Set usage limits
this.setUsageLimits(limits);
}
setUsageLimits(limits) {
// Keys here must be from the server's fixed allowlist:
// users, seats, admins, projects, environments, tenants,
// api_calls_per_day, rate_limit_rps, concurrent_sessions, features.
// There is no storage-related limit key. `??`, not `||`: a limit of 0
// means none allowed, not unlimited.
this.limits = {
maxUsers: limits.users ?? Infinity,
maxAPICallsPerDay: limits.api_calls_per_day ?? Infinity,
maxConcurrentSessions: limits.concurrent_sessions ?? Infinity,
maxProjects: limits.projects ?? Infinity
};
}
isFeatureEnabled(featureName) {
return this.features.get(featureName) || false;
}
checkUsageLimit(limitType, currentUsage) {
const limit = this.limits[limitType];
if (limit === Infinity) return { allowed: true };
const allowed = currentUsage < limit;
const remaining = Math.max(0, limit - currentUsage);
const percentage = limit > 0 ? (currentUsage / limit) * 100 : 100;
return {
allowed,
limit,
current: currentUsage,
remaining,
percentage
};
}
getFeatureList() {
return Array.from(this.features.entries()).map(([name, enabled]) => ({
name,
enabled
}));
}
getLimits() {
return { ...this.limits };
}
}
// Usage in application
class MyApplication {
constructor(licenseKey, productUuid) {
this.licenseKey = licenseKey;
this.productUuid = productUuid;
this.featureManager = null;
}
async initialize() {
// RobustLicenseVerifier from Persistent Caching above
const verifier = new RobustLicenseVerifier('https://your-license-api.com/v1', this.productUuid);
const result = await verifier.verifyLicense(this.licenseKey);
if (result.outcome === 'refused') {
throw new Error(`License refused: ${result.error}`);
}
if (result.outcome === 'no-answer') {
throw new Error(`Can't reach the license server: ${result.error}`);
}
this.featureManager = new FeatureManager(result.license);
console.log('Available features:', this.featureManager.getFeatureList());
console.log('Usage limits:', this.featureManager.getLimits());
}
async createProject(projectData) {
// Check if feature is enabled
if (!this.featureManager.isFeatureEnabled('basic_features')) {
throw new Error('Project creation not available in your license');
}
// Check usage limits
const currentProjects = await this.getCurrentProjectCount();
const limitCheck = this.featureManager.checkUsageLimit('maxProjects', currentProjects);
if (!limitCheck.allowed) {
throw new Error(`Project limit reached: ${limitCheck.current}/${limitCheck.limit}`);
}
// Create project
return this.doCreateProject(projectData);
}
async makeAPICall() {
const currentCalls = await this.getCurrentAPICallCount();
const limitCheck = this.featureManager.checkUsageLimit('maxAPICallsPerDay', currentCalls);
if (!limitCheck.allowed) {
throw new Error(`API call limit reached: ${limitCheck.current}/${limitCheck.limit}`);
}
if (limitCheck.percentage > 80) {
console.warn(`API usage warning: ${limitCheck.percentage.toFixed(1)}% of limit used`);
}
return this.doMakeAPICall();
}
// Placeholder methods
async getCurrentProjectCount() { return 0; }
async getCurrentAPICallCount() { return 0; }
async doCreateProject(data) { return { id: 1, ...data }; }
async doMakeAPICall() { return { success: true }; }
}For general integration best practices (error handling, performance, security, monitoring), see the Integration Checklist.
Next Steps
- License Activation - Track instance activations
- Usage Tracking - Monitor license usage
- Client Examples - Implementation examples in various languages
- Offline license verification - How public-key verification works, and what it can and can't prove