Skip to content

License Verification

Detailed guide on implementing license verification in your applications, including caching strategies, offline support, and best practices.

Basic License Verification

Simple Verification

The most basic license verification checks if a key is valid:

bash
curl -X POST https://your-license-api.com/v1/verify-license \
  -H "Content-Type: application/json" \
  -d '{
    "key": "7yChyZcfMG23Dx1sjBoLziPFrH4n-6f28"
  }'

Response:

json
{
  "valid": true,
  "tier": "pro",
  "product_id": "your-product",
  "limits": {
    "users": 100,
    "api_calls_per_day": 10000
  },
  "expires_at": "2025-12-31T23:59:59Z",
  "issued_at": "2025-01-01T00:00:00Z",
  "status": "active"
}

Verification with Instance Tracking

For applications that need activation tracking:

bash
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 license when your application starts:

javascript
class ApplicationBootstrap {
  constructor(licenseKey, apiUrl) {
    this.licenseKey = licenseKey;
    this.apiUrl = apiUrl;
    this.licenseInfo = null;
  }

  async initialize() {
    try {
      // Verify license before starting application
      const verification = await this.verifyLicense();
      
      if (!verification.valid) {
        throw new Error(`Invalid license: ${verification.error}`);
      }

      this.licenseInfo = verification;
      // Note: verify-license deliberately omits issued_to from its
      // response (a privacy choice) - it's always undefined here. Use
      // GET /v1/admin/license/:key (admin-authenticated) if you need it.
      console.log(`License tier: ${verification.tier}`);
      
      // Start application with license constraints
      this.startApplication(verification);
      
    } catch (error) {
      console.error('License verification failed:', error.message);
      this.showLicenseError(error);
    }
  }

  async verifyLicense() {
    const response = await fetch(`${this.apiUrl}/verify-license`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ key: this.licenseKey })
    });

    return response.json();
  }

  startApplication(licenseInfo) {
    // Apply license limits to application features
    if (licenseInfo.limits.users) {
      this.setMaxUsers(licenseInfo.limits.users);
    }
    
    if (licenseInfo.limits.api_calls_per_day) {
      this.setAPICallLimit(licenseInfo.limits.api_calls_per_day);
    }

    console.log('Application started successfully');
  }
}

// Usage
const app = new ApplicationBootstrap(
  process.env.LICENSE_KEY,
  'https://your-license-api.com/v1'
);

app.initialize();

2. Periodic Verification

Continuously verify license validity during runtime:

javascript
class PeriodicLicenseChecker {
  constructor(licenseKey, apiUrl, intervalMinutes = 60) {
    this.licenseKey = licenseKey;
    this.apiUrl = apiUrl;
    this.interval = intervalMinutes * 60 * 1000;
    this.isRunning = false;
    this.intervalId = null;
    this.lastVerification = null;
  }

  start() {
    if (this.isRunning) return;
    
    this.isRunning = true;
    console.log(`Starting periodic license verification every ${this.interval / 60000} minutes`);
    
    // Initial check
    this.checkLicense();
    
    // Schedule periodic checks
    this.intervalId = setInterval(() => {
      this.checkLicense();
    }, this.interval);
  }

  stop() {
    if (!this.isRunning) return;
    
    this.isRunning = false;
    
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
    
    console.log('Stopped periodic license verification');
  }

  async checkLicense() {
    try {
      const verification = await this.verifyLicense();
      this.lastVerification = {
        timestamp: new Date(),
        result: verification
      };

      if (!verification.valid) {
        console.error('License verification failed:', verification.error);
        this.handleInvalidLicense(verification);
      } else {
        console.log('License verification successful');
        this.handleValidLicense(verification);
      }

    } catch (error) {
      console.error('License verification error:', error.message);
      this.handleVerificationError(error);
    }
  }

  async verifyLicense() {
    const response = await fetch(`${this.apiUrl}/verify-license`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ key: this.licenseKey })
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    return response.json();
  }

  handleValidLicense(verification) {
    // Check if license is expiring soon
    if (verification.expires_at) {
      const expiryDate = new Date(verification.expires_at);
      const daysUntilExpiry = (expiryDate - new Date()) / (1000 * 60 * 60 * 24);
      
      if (daysUntilExpiry <= 30) {
        console.warn(`License expires in ${Math.floor(daysUntilExpiry)} days`);
        this.notifyExpiryWarning(daysUntilExpiry);
      }
    }
  }

  handleInvalidLicense(verification) {
    // The API has no `expired`/`revoked` boolean fields -- an invalid
    // license is always just {valid: false, error: "<message>"}, so branch
    // on the message text instead. Note the server doesn't distinguish
    // "revoked" from other non-active states in its message -- a revoked
    // license and, say, a suspended one both come back as "License is not
    // active", so this can't reliably tell revoked apart from other cases.
    if (verification.error?.includes('expired')) {
      this.handleExpiredLicense();
    } else if (verification.error?.includes('not active')) {
      this.handleRevokedLicense();
    } else {
      this.handleGenericInvalidLicense();
    }
  }

  handleVerificationError(error) {
    // Network or server errors - don't immediately shut down
    const gracePeriodHours = 4;
    const lastSuccess = this.getLastSuccessfulVerification();
    
    if (lastSuccess && (Date.now() - lastSuccess) < gracePeriodHours * 60 * 60 * 1000) {
      console.warn(`License verification failed, but within grace period. Continuing operation.`);
    } else {
      console.error('Grace period exceeded. Application should shut down.');
      this.initiateGracefulShutdown();
    }
  }

  handleExpiredLicense() {
    console.error('License has expired');
    this.showExpiryDialog();
    // Optionally shut down application
  }

  handleRevokedLicense() {
    console.error('License has been revoked');
    this.initiateGracefulShutdown();
  }

  handleGenericInvalidLicense() {
    console.error('License is invalid');
    this.showLicenseErrorDialog();
  }

  getLastSuccessfulVerification() {
    return this.lastVerification?.result?.valid ? 
      this.lastVerification.timestamp.getTime() : null;
  }

  notifyExpiryWarning(daysRemaining) {
    // Show user notification about upcoming expiry
    console.warn(`License expires in ${Math.floor(daysRemaining)} days`);
  }

  showExpiryDialog() {
    // Show expiry dialog to user
    console.log('Please renew your license');
  }

  showLicenseErrorDialog() {
    // Show license error dialog
    console.log('License error - please contact support');
  }

  initiateGracefulShutdown() {
    console.log('Initiating graceful shutdown due to license issues');
    // Implement graceful shutdown logic
    process.exit(1);
  }
}

// Usage
const licenseChecker = new PeriodicLicenseChecker(
  process.env.LICENSE_KEY,
  'https://your-license-api.com/v1',
  60 // Check every hour
);

licenseChecker.start();

// Graceful shutdown
process.on('SIGTERM', () => {
  licenseChecker.stop();
});

Caching Strategies

1. In-Memory Caching

Cache verification results to reduce API calls:

javascript
class CachedLicenseVerifier {
  constructor(apiUrl, cacheMinutes = 30) {
    this.apiUrl = apiUrl;
    this.cache = new Map();
    this.cacheTTL = cacheMinutes * 60 * 1000;
  }

  async verifyLicense(key) {
    // Check cache first
    const cached = this.cache.get(key);
    if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
      console.log('Returning cached license verification');
      return { ...cached.result, fromCache: true };
    }

    try {
      // Verify with API
      const result = await this.verifyWithAPI(key);
      
      // Cache successful results
      if (result.valid) {
        this.cache.set(key, {
          result,
          timestamp: Date.now()
        });
      }

      return result;

    } catch (error) {
      // If API fails, return cached result if available
      if (cached) {
        console.warn('API verification failed, using cached result');
        return { ...cached.result, fromCache: true, warning: 'Offline verification' };
      }
      throw error;
    }
  }

  async verifyWithAPI(key) {
    const response = await fetch(`${this.apiUrl}/verify-license`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ key })
    });

    if (!response.ok) {
      throw new Error(`API Error: ${response.status}`);
    }

    return response.json();
  }

  clearCache(key = null) {
    if (key) {
      this.cache.delete(key);
    } else {
      this.cache.clear();
    }
  }

  getCacheStats() {
    const now = Date.now();
    let valid = 0, expired = 0;

    for (const [key, cached] of this.cache.entries()) {
      if (now - cached.timestamp < this.cacheTTL) {
        valid++;
      } else {
        expired++;
      }
    }

    return { total: this.cache.size, valid, expired };
  }
}

2. Persistent Caching

Store verification results on disk for offline support:

javascript
const fs = require('fs').promises;
const path = require('path');

class PersistentLicenseCache {
  constructor(cacheDir = './license-cache') {
    this.cacheDir = cacheDir;
    this.ensureCacheDir();
  }

  async ensureCacheDir() {
    try {
      await fs.mkdir(this.cacheDir, { recursive: true });
    } catch (error) {
      console.error('Failed to create cache directory:', error);
    }
  }

  getCacheFilePath(key) {
    const hasher = require('crypto').createHash('sha256');
    hasher.update(key);
    const keyHash = hasher.digest('hex').substring(0, 16);
    return path.join(this.cacheDir, `license-${keyHash}.json`);
  }

  async getCachedVerification(key, maxAgeMinutes = 60) {
    try {
      const filePath = this.getCacheFilePath(key);
      const data = await fs.readFile(filePath, 'utf8');
      const cached = JSON.parse(data);

      const age = Date.now() - cached.timestamp;
      const maxAge = maxAgeMinutes * 60 * 1000;

      if (age < maxAge) {
        return { ...cached.result, fromCache: true };
      } else {
        // Cache expired
        await this.removeCachedVerification(key);
        return null;
      }

    } catch (error) {
      // Cache file doesn't exist or is corrupted
      return null;
    }
  }

  async setCachedVerification(key, result) {
    try {
      const filePath = this.getCacheFilePath(key);
      const cached = {
        result,
        timestamp: Date.now(),
        key: key.substring(0, 8) + '...' // Partial key for identification
      };

      await fs.writeFile(filePath, JSON.stringify(cached, null, 2));
      
    } catch (error) {
      console.error('Failed to cache verification:', error);
    }
  }

  async removeCachedVerification(key) {
    try {
      const filePath = this.getCacheFilePath(key);
      await fs.unlink(filePath);
    } catch (error) {
      // File might not exist, ignore error
    }
  }

  async clearAllCache() {
    try {
      const files = await fs.readdir(this.cacheDir);
      const licenseFiles = files.filter(f => f.startsWith('license-'));
      
      await Promise.all(
        licenseFiles.map(file => 
          fs.unlink(path.join(this.cacheDir, file))
        )
      );

      console.log(`Cleared ${licenseFiles.length} cached license files`);
      
    } catch (error) {
      console.error('Failed to clear cache:', error);
    }
  }

  async getCacheInfo() {
    try {
      const files = await fs.readdir(this.cacheDir);
      const licenseFiles = files.filter(f => f.startsWith('license-'));
      
      const cacheInfo = [];
      
      for (const file of licenseFiles) {
        try {
          const filePath = path.join(this.cacheDir, file);
          const data = await fs.readFile(filePath, 'utf8');
          const cached = JSON.parse(data);
          
          cacheInfo.push({
            file,
            key: cached.key,
            timestamp: new Date(cached.timestamp),
            age: Date.now() - cached.timestamp,
            valid: cached.result.valid
          });
        } catch (error) {
          console.warn(`Corrupted cache file: ${file}`);
        }
      }

      return cacheInfo;
      
    } catch (error) {
      console.error('Failed to get cache info:', error);
      return [];
    }
  }
}

// Combined verifier with persistent cache
class RobustLicenseVerifier {
  constructor(apiUrl) {
    this.apiUrl = apiUrl;
    this.cache = new PersistentLicenseCache();
  }

  async verifyLicense(key, options = {}) {
    const { 
      useCache = true, 
      cacheMinutes = 60, 
      offlineMode = false 
    } = options;

    // Try cache first if enabled
    if (useCache) {
      const cached = await this.cache.getCachedVerification(key, cacheMinutes);
      if (cached) {
        console.log('Using cached license verification');
        return cached;
      }
    }

    // If offline mode, return failure if no cache
    if (offlineMode) {
      return {
        valid: false,
        error: 'Offline mode and no cached verification available'
      };
    }

    try {
      // Verify online
      const result = await this.verifyOnline(key);
      
      // Cache successful verifications
      if (result.valid && useCache) {
        await this.cache.setCachedVerification(key, result);
      }

      return result;

    } catch (error) {
      // API failed, try to use cached result
      if (useCache) {
        const cached = await this.cache.getCachedVerification(key, cacheMinutes * 10); // Extended grace period
        if (cached) {
          console.warn('API verification failed, using cached result');
          return { 
            ...cached, 
            warning: 'Verification failed, using cached result' 
          };
        }
      }

      throw error;
    }
  }

  async verifyOnline(key) {
    const response = await fetch(`${this.apiUrl}/verify-license`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ key })
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    return response.json();
  }
}

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 GET /v1/export-license/:key (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.

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:

bash
curl https://your-license-api.com/v1/public-key
json
{ "publicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n" }

Fetching a Signed License

bash
curl https://your-license-api.com/v1/export-license/7yChyZcfMG23Dx1sjBoLziPFrH4n-6f28/offline
json
{
  "license": {
    "key": "7yChyZcfMG23Dx1sjBoLziPFrH4n-6f28",
    "tier": "pro",
    "product_id": "my-product",
    "expires_at": "2024-12-31T23:59:59.000Z",
    "limits": {}
  },
  "signature": "base64-encoded-rsa-signature"
}

Verifying Fully Offline

javascript
import { createVerify } from 'node:crypto';

function verifyOfflineLicense(license, signature, publicKey) {
  const verifier = createVerify('SHA256');
  verifier.update(JSON.stringify(license));
  verifier.end();
  return verifier.verify(publicKey, signature, 'base64');
}

This confirms the license payload hasn't been tampered with since it was exported, using only the cached public key — no server round-trip. What it does not know is whether the license has been revoked since export. If live revocation status matters for your product, use verify-license-file/verify-license-file-base64 instead, or re-fetch the offline export periodically.

Feature Gating Based on License

License-Based Feature Control

javascript
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.
    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 = (currentUsage / limit) * 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) {
    this.licenseKey = licenseKey;
    this.featureManager = null;
  }

  async initialize() {
    // Verify license
    const verifier = new RobustLicenseVerifier('https://your-license-api.com/v1');
    const licenseInfo = await verifier.verifyLicense(this.licenseKey);

    if (!licenseInfo.valid) {
      throw new Error('Invalid license');
    }

    // Initialize feature management
    this.featureManager = new FeatureManager(licenseInfo);
    
    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