License Activation
Comprehensive guide to implementing license activation and instance tracking in your applications.
Overview
License activation allows you to track which instances are using a license, enforce activation limits, and provide better license management for your customers.
Basic Activation
Simple Activation
Activate a license for a specific instance:
curl -X POST https://your-license-api.com/v1/activate-license \
-H "Content-Type: application/json" \
-d '{
"key": "7yChyZcfMG23Dx1sjBoLziPFrH4n-6f28",
"instance_id": "server-001"
}'Response (first activation of this key/instance_id pair):
{
"activated": true,
"token": "b6f2b6e6a4f4e6e0b8f2b6e6a4f4e6e0"
}token is the opaque credential this instance needs to later call deactivate-license — it's only returned on the call that creates the activation. Store it. If the same key/instance_id pair activates again (e.g. app restart), the response is instead:
{
"activated": true,
"alreadyActivated": true
}with no token — the server doesn't re-issue or re-expose it, so hold on to the one you got the first time.
Key vs. token — which one actually needs protecting
The license key isn't a secret — it's meant to live in a distributed app, a config file, even public page source, and activate-license itself accepts nothing but {key, instance_id} to prove that. The token returned above is the real credential: it's what deactivate-license and track-usage require afterward as proof this specific instance holds a real activation, not just a guessed instance_id. It's generated once, shown to you exactly once (here), and never retrievable from the server again — there's no GET for it.
If your app loses its token (a wiped device, a corrupted config), the activation itself is still fully valid — verification and license enforcement don't need the token at all. The instance just can't self-service-deactivate or report usage anymore until an admin mints it a fresh one via POST /v1/admin/reissue-token, which invalidates the old token in the same call. There's no self-service recovery for a lost token by design — that's exactly the credential an attacker would also want, so only an admin can reissue one.
Instance metadata
The request body accepts only key and instance_id — nothing else. An earlier version of this guide showed an instance_info object (hostname, IP, platform, etc.) for "better tracking," but the server has never read or stored that field; it's silently discarded. If you need to correlate activations with instance metadata, track it in your own system alongside the instance_id you generate.
Instance ID Generation
Unique Instance Identifiers
Generate consistent, unique instance IDs:
const os = require('os');
const crypto = require('crypto');
class InstanceIDGenerator {
static generate(includeTimestamp = false) {
const hostname = os.hostname();
const platform = os.platform();
const arch = os.arch();
const networkInterfaces = os.networkInterfaces();
// Get MAC address from primary network interface
const primaryInterface = this.getPrimaryNetworkInterface(networkInterfaces);
const macAddress = primaryInterface?.mac || 'unknown';
// Create base identifier
const baseId = `${hostname}-${platform}-${arch}-${macAddress}`;
// Add timestamp if requested (for unique instances)
const uniqueId = includeTimestamp ?
`${baseId}-${Date.now()}` :
baseId;
// Hash to create consistent, shorter ID
return crypto
.createHash('sha256')
.update(uniqueId)
.digest('hex')
.substring(0, 16);
}
static getPrimaryNetworkInterface(interfaces) {
// Find the primary network interface (usually eth0, en0, etc.)
const priorities = ['eth0', 'en0', 'wlan0', 'wi-fi'];
for (const name of priorities) {
if (interfaces[name]) {
return interfaces[name].find(iface => !iface.internal);
}
}
// Fallback to first non-internal interface
for (const [name, ifaces] of Object.entries(interfaces)) {
const external = ifaces.find(iface => !iface.internal);
if (external) {
return external;
}
}
return null;
}
static generateForContainer() {
// For containerized environments
const containerId = process.env.HOSTNAME ||
process.env.CONTAINER_ID ||
crypto.randomBytes(8).toString('hex');
return crypto
.createHash('sha256')
.update(`container-${containerId}`)
.digest('hex')
.substring(0, 16);
}
static generateForCloud(provider = 'aws') {
// For cloud environments - fetch instance metadata
switch (provider.toLowerCase()) {
case 'aws':
return this.generateAWSInstanceId();
case 'gcp':
return this.generateGCPInstanceId();
case 'azure':
return this.generateAzureInstanceId();
default:
return this.generate(true);
}
}
static async generateAWSInstanceId() {
try {
const fetch = require('node-fetch');
const response = await fetch('http://169.254.169.254/latest/meta-data/instance-id', {
timeout: 2000
});
if (response.ok) {
const instanceId = await response.text();
return `aws-${instanceId}`;
}
} catch (error) {
console.warn('Failed to fetch AWS instance ID:', error.message);
}
return this.generate(true);
}
// Similar methods for GCP and Azure...
}
// Usage
const instanceId = InstanceIDGenerator.generate();
console.log('Instance ID:', instanceId);Platform-Specific Generators
class PlatformSpecificInstanceId {
static generate() {
const platform = os.platform();
switch (platform) {
case 'win32':
return this.generateWindows();
case 'darwin':
return this.generateMacOS();
case 'linux':
return this.generateLinux();
default:
return this.generateGeneric();
}
}
static generateWindows() {
const { execSync } = require('child_process');
try {
// Get Windows machine GUID
const output = execSync(
'wmic csproduct get uuid /format:value',
{ encoding: 'utf8', timeout: 5000 }
);
const match = output.match(/UUID=(.+)/);
if (match && match[1] && match[1].trim() !== '') {
return `win-${match[1].trim()}`;
}
} catch (error) {
console.warn('Failed to get Windows UUID:', error.message);
}
return this.generateGeneric();
}
static generateMacOS() {
const { execSync } = require('child_process');
try {
// Get macOS hardware UUID
const output = execSync(
'system_profiler SPHardwareDataType | grep "Hardware UUID"',
{ encoding: 'utf8', timeout: 5000 }
);
const match = output.match(/Hardware UUID:\s*(.+)/);
if (match && match[1]) {
return `mac-${match[1].trim()}`;
}
} catch (error) {
console.warn('Failed to get macOS UUID:', error.message);
}
return this.generateGeneric();
}
static generateLinux() {
const fs = require('fs');
try {
// Try to read machine-id
if (fs.existsSync('/etc/machine-id')) {
const machineId = fs.readFileSync('/etc/machine-id', 'utf8').trim();
return `linux-${machineId}`;
}
// Fallback to DMI product UUID
if (fs.existsSync('/sys/class/dmi/id/product_uuid')) {
const productUuid = fs.readFileSync('/sys/class/dmi/id/product_uuid', 'utf8').trim();
return `linux-${productUuid}`;
}
} catch (error) {
console.warn('Failed to get Linux machine ID:', error.message);
}
return this.generateGeneric();
}
static generateGeneric() {
return InstanceIDGenerator.generate(true);
}
}Complete Activation Implementation
Robust Activation Manager
class LicenseActivationManager {
constructor(licenseKey, apiUrl, options = {}) {
this.licenseKey = licenseKey;
this.apiUrl = apiUrl;
this.instanceId = options.instanceId || InstanceIDGenerator.generate();
this.maxRetries = options.maxRetries || 3;
this.retryDelay = options.retryDelay || 1000;
this.isActivated = false;
this.activationInfo = null;
this.token = null;
}
async activate() {
if (this.isActivated) {
return this.activationInfo;
}
try {
const result = await this.performActivation();
if (result.activated) {
this.isActivated = true;
this.activationInfo = result;
// `token` is only present in the response on the call that
// creates the activation — an already-activated replay omits
// it, so don't overwrite a previously-captured token with
// undefined.
this.token = result.token || this.token;
console.log(`License activated for instance: ${this.instanceId}`);
if (result.alreadyActivated) {
console.log('Instance was already activated for this license');
}
return result;
} else {
throw new Error(result.error || 'Activation failed');
}
} catch (error) {
console.error('License activation failed:', error.message);
throw error;
}
}
async performActivation(attempt = 1) {
try {
const response = await fetch(`${this.apiUrl}/activate-license`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
key: this.licenseKey,
instance_id: this.instanceId
}),
timeout: 30000
});
if (!response.ok) {
const error = await response.json();
if (response.status === 403 && error.error?.includes('Activation limit exceeded')) {
throw new Error(`Activation limit reached: ${error.error}`);
}
throw new Error(`HTTP ${response.status}: ${error.error || response.statusText}`);
}
return await response.json();
} catch (error) {
if (attempt < this.maxRetries && this.isRetriableError(error)) {
console.warn(`Activation attempt ${attempt} failed, retrying in ${this.retryDelay}ms...`);
await this.sleep(this.retryDelay * attempt);
return this.performActivation(attempt + 1);
}
throw error;
}
}
// Not sent to the license server (it only accepts `key`/`instance_id`,
// see the "Instance metadata" note above) -- this is for your own
// local logs/database if you want to correlate an instance_id with
// more detail than the ID alone carries.
gatherInstanceInfo() {
const os = require('os');
return {
hostname: os.hostname(),
platform: os.platform(),
arch: os.arch(),
node_version: process.version,
uptime: os.uptime(),
total_memory: os.totalmem(),
cpu_count: os.cpus().length,
network_interfaces: this.getNetworkInfo(),
process_id: process.pid,
activation_time: new Date().toISOString()
};
}
getNetworkInfo() {
const interfaces = os.networkInterfaces();
const info = {};
for (const [name, ifaces] of Object.entries(interfaces)) {
info[name] = ifaces
.filter(iface => !iface.internal)
.map(iface => ({
address: iface.address,
family: iface.family,
mac: iface.mac
}));
}
return info;
}
isRetriableError(error) {
// Retry on network errors, not on license-specific errors (these are
// the exact messages activate-license returns for each 403/404 case)
return !error.message.includes('Activation limit exceeded') &&
!error.message.includes('License key not found') &&
!error.message.includes('License has expired') &&
!error.message.includes('License has been revoked');
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async deactivate() {
if (!this.isActivated) {
return { deactivated: false, error: 'Not activated' };
}
if (!this.token) {
// Can happen if this instance was activated by a previous process
// that didn't persist the token — there's no way to recover it;
// deactivation requires either that original token or an admin
// revoking the whole license instead.
throw new Error('No activation token available — cannot deactivate');
}
try {
const response = await fetch(`${this.apiUrl}/deactivate-license`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
key: this.licenseKey,
token: this.token
}),
timeout: 30000
});
const result = await response.json();
if (result.deactivated) {
this.isActivated = false;
this.activationInfo = null;
console.log(`License deactivated for instance: ${this.instanceId}`);
}
return result;
} catch (error) {
console.error('License deactivation failed:', error.message);
throw error;
}
}
async validateActivation() {
if (!this.isActivated) {
return { valid: false, error: 'Not activated' };
}
try {
const response = await fetch(`${this.apiUrl}/validate-license`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
key: this.licenseKey,
instance_id: this.instanceId
}),
timeout: 30000
});
return await response.json();
} catch (error) {
console.error('License validation failed:', error.message);
return { valid: false, error: error.message };
}
}
getActivationInfo() {
return this.activationInfo;
}
getInstanceId() {
return this.instanceId;
}
isLicenseActivated() {
return this.isActivated;
}
}Activation Lifecycle Management
Application Integration
class ApplicationWithLicenseActivation {
constructor(licenseKey, apiUrl) {
this.activationManager = new LicenseActivationManager(licenseKey, apiUrl);
this.isRunning = false;
this.validationInterval = null;
}
async start() {
try {
// Activate license
console.log('Activating license...');
const activation = await this.activationManager.activate();
console.log('License activation successful');
if (activation.alreadyActivated) {
console.log('(reused an existing activation for this instance)');
}
// Start application
this.isRunning = true;
await this.startApplication();
// Start periodic validation
this.startPeriodicValidation();
// Setup graceful shutdown
this.setupShutdownHandlers();
} catch (error) {
console.error('Failed to start application:', error.message);
process.exit(1);
}
}
async startApplication() {
console.log('Starting application services...');
// Start your application logic here
console.log('Application started successfully');
}
startPeriodicValidation(intervalMinutes = 60) {
console.log(`Starting license validation every ${intervalMinutes} minutes`);
this.validationInterval = setInterval(async () => {
try {
const validation = await this.activationManager.validateActivation();
if (!validation.valid) {
console.error('License validation failed:', validation.error);
await this.handleInvalidLicense();
} else {
console.log('License validation successful');
}
} catch (error) {
console.warn('License validation error:', error.message);
// Don't immediately shut down on network errors
}
}, intervalMinutes * 60 * 1000);
}
async handleInvalidLicense() {
console.log('License is no longer valid - initiating shutdown');
await this.gracefulShutdown();
}
setupShutdownHandlers() {
const gracefulShutdown = async (signal) => {
console.log(`Received ${signal} - shutting down gracefully`);
await this.gracefulShutdown();
};
process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);
process.on('SIGQUIT', gracefulShutdown);
}
async gracefulShutdown() {
if (!this.isRunning) return;
console.log('Starting graceful shutdown...');
this.isRunning = false;
// Stop periodic validation
if (this.validationInterval) {
clearInterval(this.validationInterval);
this.validationInterval = null;
}
try {
// Stop application services
await this.stopApplication();
// Deactivate license
console.log('Deactivating license...');
await this.activationManager.deactivate();
console.log('License deactivated');
} catch (error) {
console.error('Error during shutdown:', error.message);
} finally {
console.log('Shutdown complete');
process.exit(0);
}
}
async stopApplication() {
console.log('Stopping application services...');
// Stop your application logic here
console.log('Application stopped');
}
}
// Usage
const app = new ApplicationWithLicenseActivation(
process.env.LICENSE_KEY,
'https://your-license-api.com/v1'
);
app.start();Activation Management for Different Scenarios
Desktop Applications
class DesktopAppActivation extends LicenseActivationManager {
constructor(licenseKey, apiUrl) {
super(licenseKey, apiUrl, {
instanceId: InstanceIDGenerator.generate() // Persistent per machine
});
this.configFile = this.getConfigFilePath();
}
getConfigFilePath() {
const os = require('os');
const path = require('path');
return path.join(os.homedir(), '.myapp', 'license.json');
}
async activate() {
// Try to load existing activation
const existing = await this.loadStoredActivation();
if (existing && await this.validateStoredActivation(existing)) {
this.isActivated = true;
this.activationInfo = existing;
console.log('Using existing license activation');
return existing;
}
// Perform new activation
const result = await super.activate();
// Store activation info
await this.storeActivation(result);
return result;
}
async loadStoredActivation() {
const fs = require('fs').promises;
try {
const data = await fs.readFile(this.configFile, 'utf8');
return JSON.parse(data);
} catch (error) {
return null;
}
}
async storeActivation(activation) {
const fs = require('fs').promises;
const path = require('path');
// The server's response doesn't echo back instance_id or a timestamp
// (it's just {activated, token} or {activated, alreadyActivated}), so
// record those ourselves alongside it for the local validity check below.
const record = {
...activation,
instanceId: this.instanceId,
activatedAt: new Date().toISOString()
};
try {
await fs.mkdir(path.dirname(this.configFile), { recursive: true });
await fs.writeFile(this.configFile, JSON.stringify(record, null, 2));
} catch (error) {
console.warn('Failed to store activation:', error.message);
}
}
async validateStoredActivation(activation) {
// Quick local validation of stored activation -- this only confirms
// the file matches this machine and looks well-formed. It doesn't
// confirm the server still considers the license valid; that still
// requires calling validateActivation() periodically.
return activation.activated &&
activation.instanceId === this.instanceId &&
new Date(activation.activatedAt).getTime() > 0;
}
}Server Applications
class ServerAppActivation extends LicenseActivationManager {
constructor(licenseKey, apiUrl, serverConfig = {}) {
const instanceId = serverConfig.instanceId ||
process.env.INSTANCE_ID ||
InstanceIDGenerator.generate();
super(licenseKey, apiUrl, { instanceId });
this.serverConfig = serverConfig;
this.healthCheckInterval = null;
}
async activate() {
const result = await super.activate();
// Start health reporting
this.startHealthReporting();
return result;
}
gatherInstanceInfo() {
const baseInfo = super.gatherInstanceInfo();
return {
...baseInfo,
server_type: this.serverConfig.type || 'unknown',
server_role: this.serverConfig.role || 'primary',
datacenter: this.serverConfig.datacenter || 'unknown',
environment: process.env.NODE_ENV || 'development',
load_balancer_id: this.serverConfig.loadBalancerId,
cluster_id: this.serverConfig.clusterId
};
}
startHealthReporting(intervalMinutes = 30) {
this.healthCheckInterval = setInterval(async () => {
try {
await this.reportHealth();
} catch (error) {
console.warn('Health reporting failed:', error.message);
}
}, intervalMinutes * 60 * 1000);
}
async reportHealth() {
const health = this.gatherHealthInfo();
try {
console.log('Instance health:', health);
} catch (error) {
console.warn('Health check failed:', error.message);
}
}
gatherHealthInfo() {
const os = require('os');
return {
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory_usage: process.memoryUsage(),
cpu_usage: process.cpuUsage(),
system_load: os.loadavg(),
free_memory: os.freemem(),
total_memory: os.totalmem()
};
}
async deactivate() {
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval);
this.healthCheckInterval = null;
}
return super.deactivate();
}
}Handling Activation Limits
Smart Activation Management
An earlier version of this pattern auto-discovered an inactive instance via admin-only GET /v1/list-activations/:key and force-freed its seat by instance_id, using POST /v1/deactivate-license. That specific approach never actually worked: deactivate-license requires the opaque token issued to the specific instance at activation time (see License Activation above), and that token is never exposed again anywhere — including in the list-activations response, which returns only instance_id and activated_at. An admin discovering an inactive instance this way has no way to obtain its token, so it can't be freed through deactivate-license.
There is now a real admin-only recovery path for exactly this case, though: POST /v1/admin/deactivate-by-instance-id force-frees a seat by key/instance_id directly, with no token required — the same admin credential that already gates list-activations is sufficient. This class discovers inactive instances via list-activations and reports them rather than force-deactivating automatically — deciding which instance to free is a real business decision (the "inactive" one might just be a laptop that's asleep, not actually abandoned), so this stays a deliberate, explicit admin action via forceDeactivateInstance() below, not something triggered automatically on every activation-limit error. POST /v1/revoke-license (invalidating the entire license, not just one seat) remains available as a separate, more drastic option, but is no longer the only one.
class SmartActivationManager extends LicenseActivationManager {
async activate() {
try {
return await super.activate();
} catch (error) {
if (error.message.includes('Activation limit exceeded')) {
return await this.handleActivationLimitReached();
}
throw error;
}
}
async handleActivationLimitReached() {
console.log('Activation limit reached, checking for inactive instances...');
// Get current activations
const activations = await this.getActivations();
if (activations.length === 0) {
throw new Error('No activations found but limit reached - contact support');
}
// Find potentially inactive instances -- reported to the admin, not
// auto-freed. Call forceDeactivateInstance() below once a human has
// decided which one (if any) is safe to free.
const inactiveInstances = this.findInactiveInstances(activations);
return this.reportActivations(activations, inactiveInstances);
}
// NOTE: GET /v1/list-activations/:key requires admin authentication —
// it returns per-device instance IDs and timestamps, which is sensitive
// enough that it isn't a public endpoint. This means this class's
// "discover inactive instances" half only works if this code is running
// somewhere that holds an admin token (e.g. your own backend, proxying
// on the customer's behalf) — not in an ordinary installed client app
// using nothing but the license key.
async getActivations() {
try {
const response = await fetch(
`${this.apiUrl}/list-activations/${this.licenseKey}`,
{
method: 'GET',
headers: { Authorization: `Bearer ${this.adminToken}` },
timeout: 30000
}
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
return data.activations || [];
} catch (error) {
console.warn('Failed to get activations:', error.message);
return [];
}
}
findInactiveInstances(activations) {
const now = new Date();
const inactiveThresholdHours = 24;
return activations.filter(activation => {
const lastSeen = new Date(activation.last_seen || activation.activated_at);
const hoursSinceLastSeen = (now - lastSeen) / (1000 * 60 * 60);
return hoursSinceLastSeen > inactiveThresholdHours;
});
}
reportActivations(activations, inactiveInstances) {
if (inactiveInstances.length > 0) {
console.log(`Found ${inactiveInstances.length} inactive instance(s):`);
inactiveInstances.forEach((activation, index) => {
console.log(`${index + 1}. ${activation.instance_id} (last seen: ${activation.last_seen || activation.activated_at})`);
});
console.log(
'To free one of these seats, call forceDeactivateInstance(instance_id) ' +
'(requires an admin token) once you\'ve confirmed it\'s safe to reclaim.'
);
}
console.log('Current license activations:');
activations.forEach((activation, index) => {
console.log(`${index + 1}. ${activation.instance_id} (activated: ${activation.activated_at})`);
});
throw new Error(
'License activation limit reached. An admin can free an inactive seat via ' +
'forceDeactivateInstance() (see the instance list above), ask the owner of an ' +
'unused instance to deactivate it themselves, or upgrade your license.\n' +
'Contact support if you need assistance managing your license activations.'
);
}
// Admin-only recovery: force-frees a seat by instance_id, with no
// token required from that instance -- the fix for the "no way to
// force-deactivate" gap this section's own intro used to describe.
// A deliberate, explicit call, not something handleActivationLimitReached()
// triggers on its own -- see that method's own comment.
async forceDeactivateInstance(instanceId) {
const response = await fetch(`${this.apiUrl}/admin/deactivate-by-instance-id`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.adminToken}`
},
body: JSON.stringify({ key: this.licenseKey, instance_id: instanceId }),
timeout: 30000
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || `HTTP ${response.status}`);
}
console.log(`Force-deactivated instance: ${instanceId}`);
return result; // { deactivated: true }
}
}Best Practices
Instance Management
- Generate consistent instance IDs across application restarts
- Include meaningful instance information for debugging
- Handle activation limit scenarios gracefully
For general integration best practices (error handling, performance, security, monitoring), see the Integration Checklist.
Next Steps
- Usage Tracking - Monitor license usage metrics
- Client Examples - Platform-specific implementations
- Error Handling - Comprehensive error management
