Skip to content

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:

bash
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):

json
{
  "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:

json
{
  "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:

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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.

javascript
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