Integration Overview
Your app talks to the License Server API: it activates a key on a machine, verifies it at launch, and can track usage and check for updates. In JavaScript, the SDK makes those calls for you; any other language calls the API directly. Managing the licenses themselves (issuing, finding, revoking, freeing a seat) happens in the console: see Using the Console.
This guide covers the SDK, the common patterns, and complete examples without it.
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.
Using the SDK
@casazium/license-sdk is the JavaScript client for the calls your app makes. It runs in Node (including Electron) and in the browser.
npm install @casazium/license-sdkIn Node, it needs version 19 or later: it calls the global fetch, and verifySignedFile() uses the global Web Crypto API, which Node has on by default from 19.
The SDK is an ES module: import it from a .mjs file, from a package with "type": "module" in its package.json, or through a bundler. The snippets in these pages also use top-level await, which needs an ES module too.
import { CasaziumLicenseClient } from '@casazium/license-sdk';
import { getFingerprint } from '@casazium/license-sdk/fingerprint';
const client = new CasaziumLicenseClient({
baseUrl: 'https://your-license-api.com/v1',
// The SDK sets no timeout of its own; this one gives up after 5 seconds.
fetch: (url, init) => fetch(url, { ...init, signal: AbortSignal.timeout(5000) }),
});
const key = 'the-customers-license-key';
const instanceId = getFingerprint(); // this machine, the same on every launch
// First run only (no token stored for this machine yet): take a seat,
// and store the token. Calling activate() again would return no token.
if (!storedToken) {
const { token } = await client.activate(key, instanceId);
// save `token` as this machine's storedToken
}
// Every launch: the license is good for your product, and this machine
// still holds its seat.
const license = await client.verifyKey(key, 'your-product-uuid');
const { valid } = await client.validate(key, instanceId);baseUrl is your API address with /v1 (see the tip above). Two more options: retries, how many times a request is retried after a network error or a 5xx (default 3), and publicKey, for checking an offline license file.
What the SDK does
| Method | API call | Covered in |
|---|---|---|
activate(key, instanceId) | POST /activate-license | License Activation |
validate(key, instanceId) (0.4.0+) | POST /validate-license | License Activation |
deactivate(key, token) | POST /deactivate-license | License Activation |
verifyKey(key, expectedProductUuid) | POST /verify-license | License Verification |
exportOfflineLicense(key), getPublicKey(), verifySignedFile(file, expectedProductUuid), exportLicense(key, expectedProductUuid) | GET /public-key; SDK 0.5.0 and later: POST /license-export/offline and POST /license-export (License Server 1.21.0+); SDK 0.4.x and earlier: GET /export-license/:key/offline and GET /export-license/:key, deprecated from 1.21.0 | Cryptographic Offline Verification |
trackUsage(key, token, metric, increment), getUsageReport(key) | POST /track-usage, POST /usage-report | Usage Tracking |
checkUpdate({ key, expectedProductUuid, platform, currentVersion, ... }) | POST /check-update | Software Distribution |
getFingerprint(), from @casazium/license-sdk/fingerprint, returns the machine's ID for instanceId. It needs Node.
Errors. A method throws when the server refuses, with the server's message and, from 0.4.0, the HTTP status on err.status: for example verifyKey failed with 403: License is not active. A network failure or timeout throws fetch's own error, with no status. verifyKey(), exportLicense() and checkUpdate() also throw when the license or release belongs to another product. License Verification shows how to tell a refusal from no answer. The SDK never retries activate() or trackUsage(), since a retry after a lost response would lose the activation token or count the usage twice.
What the SDK doesn't do. It makes the calls your app makes, not yours. Issuing, editing and revoking licenses, freeing another machine's seat, reissuing a token and registering releases are in the console, or the API with your API key. The Node build has listLicenses() and revoke(), which take an adminToken option; keep that on a server you control. The browser build leaves both out.
The SDK is JavaScript only. Any other language calls the API directly: see below.
Integration Patterns
1. Basic License Verification
The simplest integration: verify a license key before allowing access to your application.
Use case: Simple license validation at application startup.
2. License Activation Pattern
More robust pattern that tracks which instances are using a license.
Use case: Desktop applications, server software that needs activation tracking.
3. Usage Tracking Pattern
Complete integration with usage monitoring and reporting.
Use case: SaaS applications, usage-based licensing, compliance monitoring.
Without the SDK (curl or any language)
The same calls made directly, for an app in another language, or JavaScript without the SDK. Client Examples has complete clients in Python, Java and Go.
Quick Start Examples
JavaScript/Node.js
Both examples follow one rule for every response. 200 with valid: true and your own product_uuid means licensed. A 4xx other than 429 is a refusal: believe it. 429, a 5xx, a network error or a timeout is no answer: keep running on the last good check, but only for a limited time.
const axios = require('axios');
const fs = require('fs');
const os = require('os');
const path = require('path');
const crypto = require('crypto');
const STATE_FILE = path.join(os.homedir(), '.myapp', 'license-state.json');
// 'refused' for a 4xx other than 429; 'no-answer' for 429, 5xx, network
// errors and timeouts.
function classify(error) {
const status = error.response?.status;
return status >= 400 && status < 500 && status !== 429 ? 'refused' : 'no-answer';
}
class LicenseManager {
constructor(apiBaseUrl, productUuid, maxOfflineHours = 24) {
this.api = axios.create({ baseURL: apiBaseUrl, timeout: 10000 });
this.productUuid = productUuid; // your product's product_uuid, hard-coded
this.maxOffline = maxOfflineHours * 60 * 60 * 1000;
this.licenseKey = null;
// { instanceId, token, lastGoodCheck }, kept across restarts: a new
// instance ID on every launch would take a new seat every launch.
this.state = this.loadState();
this.state.instanceId ||= crypto.randomUUID();
this.saveState();
}
// { outcome: 'licensed', limits, tier, expiresAt } | { outcome: 'refused' | 'no-answer', error }
async verifyLicense(key) {
try {
const { data } = await this.api.post('/verify-license', { key });
if (data.product_uuid !== this.productUuid) {
return { outcome: 'refused', error: 'License is for a different product' };
}
this.licenseKey = key;
this.markGoodCheck();
return { outcome: 'licensed', limits: data.limits, tier: data.tier, expiresAt: data.expires_at };
} catch (error) {
return { outcome: classify(error), error: error.response?.data?.error || error.message };
}
}
// License activation for this instance. Not retried: a retry after a
// lost response gets `alreadyActivated` and no token.
async activateLicense(key) {
try {
const { data } = await this.api.post('/activate-license', {
key,
instance_id: this.state.instanceId
});
this.licenseKey = key;
if (data.token) {
this.state.token = data.token; // only returned on the first activation
this.saveState();
} else if (!this.state.token) {
console.warn('This instance holds a seat but its token is lost; free the seat in the ' +
'license portal or with POST /v1/admin/deactivate-by-instance-id, then activate again.');
}
return { outcome: 'activated', alreadyActivated: Boolean(data.alreadyActivated) };
} catch (error) {
return { outcome: classify(error), error: error.response?.data?.error || error.message };
}
}
// Is this instance still activated? 'licensed', 'refused' (seat freed,
// or license revoked or expired) or 'no-answer'.
async validateLicense() {
try {
const { data } = await this.api.post('/validate-license', {
key: this.licenseKey,
instance_id: this.state.instanceId
});
if (!data.valid) return { outcome: 'refused' };
this.markGoodCheck();
return { outcome: 'licensed' };
} catch (error) {
return { outcome: classify(error), error: error.message };
}
}
// Adds `increment` to the metric's running total on the server, which
// never resets on its own - so send what happened since the last
// report, not a running count. Requires the activation token.
async trackUsage(metric, increment) {
try {
await this.api.post('/track-usage', {
key: this.licenseKey,
token: this.state.token,
metric,
increment
});
return { outcome: 'recorded' };
} catch (error) {
return {
outcome: classify(error),
timedOut: error.code === 'ECONNABORTED', // axios's timeout
error: error.response?.data?.error || error.message
};
}
}
withinOfflineAllowance() {
return Boolean(this.state.lastGoodCheck) && Date.now() - this.state.lastGoodCheck < this.maxOffline;
}
markGoodCheck() {
this.state.lastGoodCheck = Date.now();
this.saveState();
}
loadState() {
try { return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); } catch { return {}; }
}
saveState() {
fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
fs.writeFileSync(STATE_FILE, JSON.stringify(this.state), { mode: 0o600 });
}
}
// Usage
const licenseManager = new LicenseManager(
'https://your-license-api.com/v1',
'b3f1c9a0-1234-4abc-9def-e2d4f5a6b7c8'
);
let apiCallsSinceLastReport = 0; // increment this wherever your app handles an API call
async function initializeApp() {
const licenseKey = process.env.LICENSE_KEY;
const verification = await licenseManager.verifyLicense(licenseKey);
if (verification.outcome !== 'licensed') {
console.error(verification.outcome === 'refused' ? 'License refused:' : "Can't reach the license server:", verification.error);
process.exit(1);
}
const activation = await licenseManager.activateLicense(licenseKey);
if (activation.outcome !== 'activated') {
console.error(activation.outcome === 'refused' ? 'Activation refused:' : "Can't reach the license server:", activation.error);
process.exit(1);
}
console.log('License activated successfully!');
console.log('License tier:', verification.tier);
console.log('License limits:', verification.limits);
// Hourly validation
setInterval(async () => {
const validation = await licenseManager.validateLicense();
if (validation.outcome === 'refused') {
console.error('License is no longer valid for this instance - shutting down');
process.exit(1);
}
if (validation.outcome === 'no-answer' && !licenseManager.withinOfflineAllowance()) {
console.error('No good license check for too long - shutting down');
process.exit(1);
}
}, 60 * 60 * 1000);
// Every 5 minutes, report the calls made since the last report
setInterval(async () => {
const calls = apiCallsSinceLastReport;
if (calls === 0) return; // the server refuses an increment of 0
apiCallsSinceLastReport = 0;
const result = await licenseManager.trackUsage('api_calls_per_day', calls);
if (result.outcome === 'no-answer' && !result.timedOut) {
apiCallsSinceLastReport += calls; // not counted: try again next time
} else if (result.outcome === 'no-answer') {
// A timeout: the server may have counted them, so don't resend -
// an undercount rather than a double count.
console.warn('Usage may not have been recorded:', result.error);
} else if (result.outcome === 'refused') {
console.warn('Usage not recorded:', result.error); // e.g. over the limit
}
}, 5 * 60 * 1000);
}
initializeApp();Python
import json
import os
import threading
import time
import uuid
from pathlib import Path
from typing import Any, Dict, Optional
import requests
STATE_FILE = Path.home() / '.myapp' / 'license-state.json'
def classify(status: Optional[int]) -> str:
"""'refused' for a 4xx other than 429; 'no-answer' for 429, 5xx,
network errors and timeouts (status None)."""
if status is not None and 400 <= status < 500 and status != 429:
return 'refused'
return 'no-answer'
class LicenseManager:
def __init__(self, api_base_url: str, product_uuid: str, max_offline_hours: float = 24):
self.api_base_url = api_base_url
self.product_uuid = product_uuid # your product's product_uuid, hard-coded
self.max_offline = max_offline_hours * 3600
self.license_key: Optional[str] = None
# {instance_id, token, last_good_check}, kept across restarts: a new
# instance ID on every launch would take a new seat every launch.
self.state = self._load_state()
self.state.setdefault('instance_id', uuid.uuid4().hex)
self._save_state()
self.api_calls_since_last_report = 0 # increment wherever your app handles an API call
self._lock = threading.Lock()
self.is_running = False
def _post(self, route: str, payload: Dict[str, Any]):
"""(status, body) for any HTTP answer; (None, {'error': ...}) for none."""
try:
response = requests.post(f'{self.api_base_url}/{route}', json=payload, timeout=30)
except requests.RequestException as e:
return None, {'error': f'Network error: {e}',
'timed_out': isinstance(e, requests.ReadTimeout)}
try:
body = response.json()
except ValueError:
body = {}
return response.status_code, body
def verify_license(self, key: str) -> Dict[str, Any]:
status, body = self._post('verify-license', {'key': key})
if status == 200 and body.get('valid'):
if body.get('product_uuid') != self.product_uuid:
return {'outcome': 'refused', 'error': 'License is for a different product'}
self.license_key = key
self._mark_good_check()
return {'outcome': 'licensed', 'limits': body.get('limits', {}),
'tier': body.get('tier'), 'expires_at': body.get('expires_at')}
return {'outcome': classify(status), 'error': body.get('error', f'HTTP {status}')}
def activate_license(self, key: str) -> Dict[str, Any]:
"""Not retried: a retry after a lost response gets alreadyActivated
and no token."""
status, body = self._post('activate-license',
{'key': key, 'instance_id': self.state['instance_id']})
if status != 200:
return {'outcome': classify(status), 'error': body.get('error', f'HTTP {status}')}
self.license_key = key
if body.get('token'):
self.state['token'] = body['token'] # only returned on the first activation
self._save_state()
elif not self.state.get('token'):
print('This instance holds a seat but its token is lost; free the seat in the '
'license portal or with POST /v1/admin/deactivate-by-instance-id, '
'then activate again.')
return {'outcome': 'activated', 'already_activated': body.get('alreadyActivated', False)}
def validate_license(self) -> str:
"""'licensed', 'refused' (seat freed, or license revoked or expired)
or 'no-answer'."""
status, body = self._post('validate-license',
{'key': self.license_key, 'instance_id': self.state['instance_id']})
if status == 200:
if body.get('valid'):
self._mark_good_check()
return 'licensed'
return 'refused'
return classify(status)
def track_usage(self, metric: str, increment: int) -> str:
"""Adds `increment` to the metric's running total on the server,
which never resets on its own -- send what happened since the last
report, not a running count. Requires the activation token."""
status, body = self._post('track-usage', {
'key': self.license_key, 'token': self.state.get('token'),
'metric': metric, 'increment': increment})
if status == 200:
return 'recorded'
print(f"Usage not recorded: {body.get('error', status)}")
if body.get('timed_out'):
# The server may have counted it, so it isn't resent - an
# undercount rather than a double count.
return 'unknown'
return classify(status)
def within_offline_allowance(self) -> bool:
last = self.state.get('last_good_check')
return last is not None and time.time() - last < self.max_offline
def start_background_tasks(self):
self.is_running = True
threading.Thread(target=self._validation_loop, daemon=True).start()
threading.Thread(target=self._usage_tracking_loop, daemon=True).start()
def stop_background_tasks(self):
self.is_running = False
def on_license_lost(self, reason: str):
"""Replace with your application's own shutdown."""
print(f'{reason} - shutting down')
self.is_running = False
def _validation_loop(self):
while self.is_running:
time.sleep(3600) # hourly
outcome = self.validate_license()
if outcome == 'refused':
self.on_license_lost('License is no longer valid for this instance')
elif outcome == 'no-answer' and not self.within_offline_allowance():
self.on_license_lost('No good license check for too long')
def _usage_tracking_loop(self):
while self.is_running:
time.sleep(300) # every 5 minutes
with self._lock:
calls, self.api_calls_since_last_report = self.api_calls_since_last_report, 0
if calls == 0:
continue # the server refuses an increment of 0
if self.track_usage('api_calls_per_day', calls) == 'no-answer':
with self._lock:
# not counted (429, 5xx, no connection): try again next time
self.api_calls_since_last_report += calls
def _mark_good_check(self):
self.state['last_good_check'] = time.time()
self._save_state()
def _load_state(self) -> Dict[str, Any]:
try:
return json.loads(STATE_FILE.read_text())
except (OSError, ValueError):
return {}
def _save_state(self):
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
# Created 0600 from the start: it holds the activation token.
fd = os.open(STATE_FILE, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, 'w') as f:
json.dump(self.state, f)
# Usage
def initialize_app():
license_manager = LicenseManager('https://your-license-api.com/v1',
'b3f1c9a0-1234-4abc-9def-e2d4f5a6b7c8')
# Get license key (from environment, config file, user input, etc.)
license_key = input('Enter your license key: ')
print('Verifying license...')
verification = license_manager.verify_license(license_key)
if verification['outcome'] != 'licensed':
print(f"License check failed ({verification['outcome']}): {verification['error']}")
return None
print('Activating license...')
activation = license_manager.activate_license(license_key)
if activation['outcome'] != 'activated':
print(f"License activation failed ({activation['outcome']}): {activation['error']}")
return None
print('License activated successfully!')
print(f'License tier: {verification["tier"]}')
print(f'License limits: {verification["limits"]}')
license_manager.start_background_tasks()
return license_manager
if __name__ == '__main__':
manager = initialize_app()
if manager:
print('Application started with valid license')
try:
# Your application logic here; stops when on_license_lost() runs
while manager.is_running:
time.sleep(1)
except KeyboardInterrupt:
print('Shutting down...')
else:
print('Failed to start application')Integration Checklist
Basic Implementation
- [ ] License key verification on startup
- [ ] Handle network errors gracefully
- [ ] Display meaningful error messages to users
- [ ] Respect rate limits (implement backoff/retry)
Robust Implementation
- [ ] License activation tracking
- [ ] Periodic validation checks
- [ ] Graceful shutdown on license revocation
- [ ] Cache license verification results (a refusal clears the cache; cap time offline)
- [ ] Implement exponential backoff for retries
Production Ready
- [ ] Usage tracking and reporting
- [ ] Comprehensive error handling
- [ ] Logging and monitoring integration
- [ ] License file support (if needed)
- [ ] Offline license validation (cached)
- [ ] Security best practices (secure key storage)
Common Integration Patterns
Desktop Applications
- License key entry dialog
- Activation during installation/first run
- Periodic validation (hourly/daily)
- Grace period for network issues
- License file import/export
Web Applications
- License verification on server startup
- Per-user license limits enforcement
- Usage tracking integration
- Admin dashboard for license management
Server Software
- License validation during installation
- Instance-based activation tracking
- High-availability license validation
- Usage reporting for compliance
SaaS Applications
- User registration with license key
- Feature gating based on license tier
- Real-time usage monitoring
- Automated license provisioning
Next Steps
Now that you understand the basic patterns, dive into specific implementation guides:
- License Verification - Detailed verification patterns
- License Activation - Activation and instance tracking
- Usage Tracking - Monitoring and reporting
- Client Examples - Language-specific implementations
- Licensing an Electron or Tauri desktop app - The whole flow end to end for Electron and Tauri, using the SDK
- Licensing server software you ship - On-prem products: signed license files, per-deployment seats, and never hard-failing a running system