Skip to content

Client Examples

Platform-specific implementations and examples for integrating the License Server API into various programming languages and frameworks.

JavaScript/Node.js

Complete Integration Example

javascript
const fetch = require('node-fetch');
const os = require('os');
const crypto = require('crypto');

class LicenseClient {
  constructor(licenseKey, apiUrl, options = {}) {
    this.licenseKey = licenseKey;
    this.apiUrl = apiUrl;
    this.instanceId = options.instanceId || this.generateInstanceId();
    this.isActivated = false;
    this.licenseInfo = null;
    this.usageTracker = null;
    this.token = null;
  }

  generateInstanceId() {
    const hostname = os.hostname();
    const platform = os.platform();
    return crypto
      .createHash('sha256')
      .update(`${hostname}-${platform}-${Date.now()}`)
      .digest('hex')
      .substring(0, 16);
  }

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

      // Activate license
      const activation = await this.activateLicense();
      if (!activation.activated) {
        throw new Error(`Activation failed: ${activation.error}`);
      }

      // `token` is only present on the call that creates the
      // activation — an already-activated replay omits it, so don't
      // overwrite a previously-captured token with undefined. It's
      // the credential shutdown() needs to deactivate this instance.
      this.token = activation.token || this.token;
      this.isActivated = true;
      this.licenseInfo = verification;

      // Start usage tracking
      this.startUsageTracking();

      console.log('License initialized successfully');
      return true;

    } catch (error) {
      console.error('License initialization failed:', error.message);
      return false;
    }
  }

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

    return response.json();
  }

  async activateLicense() {
    // Only `key` and `instance_id` are accepted -- the server has no
    // instance_info field to send additional metadata through.
    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
    });

    return response.json();
  }

  async validateLicense() {
    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 response.json();
  }

  // Reports one metric per call -- `metric` must already be a key in this
  // license's `limits` (see the Usage Tracking guide for the full list of
  // allowed metric names). `increment` defaults to 1 if omitted. Requires
  // this.token (the activation token captured in initialize()) -- the
  // license key alone is not accepted.
  async trackUsage(metric, increment = 1) {
    const response = await fetch(`${this.apiUrl}/track-usage`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        key: this.licenseKey,
        token: this.token,
        metric,
        increment
      }),
      timeout: 30000
    });

    return response.json();
  }

  // Returns a live snapshot of every metric this license has limits for:
  // { valid, status, expires_at, metrics: [{metric, used, limit, remaining, exceeded}] }
  async getUsageReport() {
    const response = await fetch(`${this.apiUrl}/usage-report`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ key: this.licenseKey }),
      timeout: 30000
    });

    return response.json();
  }

  startUsageTracking() {
    this.usageTracker = {
      apiCalls: 0,
      activeUsers: new Set()
    };

    // Report usage every 5 minutes -- one trackUsage() call per metric
    setInterval(async () => {
      try {
        if (this.usageTracker.apiCalls > 0) {
          await this.trackUsage('api_calls_per_day', this.usageTracker.apiCalls);
        }
        if (this.usageTracker.activeUsers.size > 0) {
          await this.trackUsage('concurrent_sessions', this.usageTracker.activeUsers.size);
        }

        // Reset counters
        this.usageTracker.apiCalls = 0;
        this.usageTracker.activeUsers.clear();
      } catch (error) {
        console.warn('Usage tracking failed:', error.message);
      }
    }, 300000);
  }

  recordAPICall() {
    if (this.usageTracker) {
      this.usageTracker.apiCalls++;
    }
  }

  recordUserActivity(userId) {
    if (this.usageTracker) {
      this.usageTracker.activeUsers.add(userId);
    }
  }

  async shutdown() {
    if (this.isActivated) {
      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) {
          console.log('License deactivated successfully');
        }

      } catch (error) {
        console.error('License deactivation failed:', error.message);
      }
    }
  }
}

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

licenseClient.initialize().then(success => {
  if (success) {
    console.log('Application ready');
    
    // Your application logic here
    licenseClient.recordAPICall();
    licenseClient.recordUserActivity('user123');
    
    // Graceful shutdown
    process.on('SIGTERM', () => {
      licenseClient.shutdown().then(() => process.exit(0));
    });
  } else {
    process.exit(1);
  }
});

Express.js Middleware

javascript
const express = require('express');

class LicenseMiddleware {
  constructor(licenseClient) {
    this.licenseClient = licenseClient;
    this.activeUsers = new Map();
  }

  // Middleware to track API calls
  trackAPICalls() {
    return (req, res, next) => {
      this.licenseClient.recordAPICall();
      next();
    };
  }

  // Middleware to track user activity
  trackUserActivity() {
    return (req, res, next) => {
      const userId = req.user?.id || req.headers['x-user-id'];
      if (userId) {
        this.licenseClient.recordUserActivity(userId);
        
        // Track session duration
        this.activeUsers.set(userId, Date.now());
      }
      next();
    };
  }

  // Middleware to enforce license limits.
  //
  // Note: `validate-license` (used by LicenseClient.validateLicense()
  // above) returns only `{valid: boolean}` -- it has no `limits` field.
  // Limits and current usage come from `usage-report` instead, so that's
  // what this checks against.
  enforceLimits() {
    return async (req, res, next) => {
      try {
        const validation = await this.licenseClient.validateLicense();

        if (!validation.valid) {
          return res.status(403).json({
            error: 'License validation failed',
            message: 'Please check your license status'
          });
        }

        const report = await this.licenseClient.getUsageReport();

        const exceeded = report.metrics.find((m) => m.exceeded);
        if (exceeded) {
          return res.status(429).json({
            error: 'Usage limit exceeded',
            limit: exceeded.metric,
            current: exceeded.used,
            max: exceeded.limit
          });
        }

        next();
      } catch (error) {
        console.error('License validation error:', error.message);
        next(); // Continue on validation errors
      }
    };
  }
}

// Usage
const app = express();
const licenseMiddleware = new LicenseMiddleware(licenseClient);

// Apply middleware
app.use(licenseMiddleware.trackAPICalls());
app.use(licenseMiddleware.trackUserActivity());
app.use('/api', licenseMiddleware.enforceLimits());

app.get('/api/data', (req, res) => {
  res.json({ data: 'Your protected data' });
});

app.listen(3000, () => {
  console.log('Server running with license protection');
});

Python

Complete Integration Example

python
import requests
import threading
import time
import hashlib
import platform
import json
import base64
from typing import Dict, Any, Optional

class LicenseClient:
    def __init__(self, license_key: str, api_url: str, instance_id: Optional[str] = None):
        self.license_key = license_key
        self.api_url = api_url.rstrip('/')
        self.instance_id = instance_id or self.generate_instance_id()
        self.is_activated = False
        self.license_info = None
        self.usage_tracker = None
        self.validation_thread = None
        self.tracking_thread = None
        self.is_running = False
        self.token = None
        
    def generate_instance_id(self) -> str:
        """Generate a unique instance identifier."""
        hostname = platform.node()
        system = platform.system()
        timestamp = str(int(time.time()))
        
        unique_string = f"{hostname}-{system}-{timestamp}"
        return hashlib.sha256(unique_string.encode()).hexdigest()[:16]
    
    def initialize(self) -> bool:
        """Initialize license verification and activation."""
        try:
            # Verify license
            verification = self.verify_license()
            if not verification.get('valid'):
                raise Exception(f"Invalid license: {verification.get('error', 'Unknown error')}")
            
            # Activate license
            activation = self.activate_license()
            if not activation.get('activated'):
                raise Exception(f"Activation failed: {activation.get('error', 'Unknown error')}")
            
            # `token` is only present on the call that creates the
            # activation — an already-activated replay omits it, so
            # don't overwrite a previously-captured token with None.
            # It's the credential shutdown() needs to deactivate this
            # instance.
            self.token = activation.get('token') or self.token
            self.is_activated = True
            self.license_info = verification
            
            # Start background tasks
            self.start_background_tasks()
            
            print("License initialized successfully")
            return True
            
        except Exception as e:
            print(f"License initialization failed: {e}")
            return False
    
    def verify_license(self) -> Dict[str, Any]:
        """Verify license key validity."""
        response = requests.post(
            f'{self.api_url}/verify-license',
            json={'key': self.license_key},
            timeout=30
        )
        return response.json()
    
    def activate_license(self) -> Dict[str, Any]:
        """Activate license for this instance. Only key/instance_id are
        accepted -- the server has no instance_info field."""
        response = requests.post(
            f'{self.api_url}/activate-license',
            json={
                'key': self.license_key,
                'instance_id': self.instance_id
            },
            timeout=30
        )
        return response.json()
    
    def validate_license(self) -> Dict[str, Any]:
        """Validate current license status."""
        response = requests.post(
            f'{self.api_url}/validate-license',
            json={
                'key': self.license_key,
                'instance_id': self.instance_id
            },
            timeout=30
        )
        return response.json()
    
    def track_usage(self, metric: str, increment: int = 1) -> Dict[str, Any]:
        """Report usage for a single metric. `metric` must already be a key
        in this license's `limits`. Requires self.token (the activation
        token captured in initialize()) -- the license key alone is not
        accepted."""
        response = requests.post(
            f'{self.api_url}/track-usage',
            json={
                'key': self.license_key,
                'token': self.token,
                'metric': metric,
                'increment': increment
            },
            timeout=30
        )
        return response.json()

    def get_usage_report(self) -> Dict[str, Any]:
        """Live snapshot of every metric this license has limits for."""
        response = requests.post(
            f'{self.api_url}/usage-report',
            json={'key': self.license_key},
            timeout=30
        )
        return response.json()

    def get_public_key(self) -> str:
        """Fetch the RSA public key used to verify offline-signed license
        files (see export_offline_license()). Intended to be fetched once
        and cached/embedded by the consuming application -- not something
        a genuinely offline client should call on every verification."""
        response = requests.get(f'{self.api_url}/public-key', timeout=30)
        return response.json()['publicKey']

    def export_offline_license(self, key: Optional[str] = None) -> Dict[str, Any]:
        """Fetch a license payload RSA-signed with the server's private
        key, in the {license, signature} shape verify_signed_license()
        expects. Unlike verify_license()/validate_license(), the returned
        signature can be checked with only the public key from
        get_public_key() -- no further server contact required."""
        response = requests.get(
            f'{self.api_url}/export-license/{key or self.license_key}/offline',
            timeout=30
        )
        return response.json()

    def start_background_tasks(self):
        """Start background validation and usage tracking."""
        self.is_running = True
        self.usage_tracker = {
            'api_calls': 0,
            'active_users': set(),
            'start_time': time.time()
        }
        
        # Start validation thread
        self.validation_thread = threading.Thread(target=self._validation_loop)
        self.validation_thread.daemon = True
        self.validation_thread.start()
        
        # Start tracking thread
        self.tracking_thread = threading.Thread(target=self._tracking_loop)
        self.tracking_thread.daemon = True
        self.tracking_thread.start()
    
    def _validation_loop(self):
        """Background license validation."""
        while self.is_running:
            try:
                validation = self.validate_license()
                if not validation.get('valid'):
                    print(f"License validation failed: {validation.get('error')}")
                    self._handle_invalid_license()
                    break
                    
                time.sleep(3600)  # Check every hour
                
            except Exception as e:
                print(f"Validation error: {e}")
                time.sleep(300)  # Retry in 5 minutes on error
    
    def _tracking_loop(self):
        """Background usage tracking -- one track_usage() call per metric."""
        while self.is_running:
            try:
                if self.usage_tracker:
                    if self.usage_tracker['api_calls'] > 0:
                        self.track_usage('api_calls_per_day', self.usage_tracker['api_calls'])
                    if len(self.usage_tracker['active_users']) > 0:
                        self.track_usage('concurrent_sessions', len(self.usage_tracker['active_users']))

                    # Reset counters
                    self.usage_tracker['api_calls'] = 0
                    self.usage_tracker['active_users'].clear()
                
                time.sleep(300)  # Report every 5 minutes
                
            except Exception as e:
                print(f"Usage tracking error: {e}")
                time.sleep(300)
    
    def _handle_invalid_license(self):
        """Handle invalid license scenario."""
        print("License is invalid - shutting down application")
        self.shutdown()
        # In a real application, you might want to exit or disable features
    
    def record_api_call(self):
        """Record an API call."""
        if self.usage_tracker:
            self.usage_tracker['api_calls'] += 1
    
    def record_user_activity(self, user_id: str):
        """Record user activity."""
        if self.usage_tracker:
            self.usage_tracker['active_users'].add(user_id)
    
    def update_storage_usage(self, bytes_used: int):
        """Update storage usage."""
        if self.usage_tracker:
            self.usage_tracker['storage_used'] = bytes_used
    
    def shutdown(self):
        """Shutdown license client and deactivate."""
        print("Shutting down license client...")
        self.is_running = False
        
        if self.is_activated:
            try:
                response = requests.post(
                    f'{self.api_url}/deactivate-license',
                    json={
                        'key': self.license_key,
                        'token': self.token
                    },
                    timeout=30
                )
                
                result = response.json()
                if result.get('deactivated'):
                    print("License deactivated successfully")
                    
            except Exception as e:
                print(f"License deactivation failed: {e}")

def verify_signed_license(license: Dict[str, Any], signature_base64: str, public_key_pem: str) -> bool:
    """Verify an RSA-SHA256 signature (base64, PKCS#1 v1.5 padding --
    matching Node's createSign('SHA256')/createVerify('SHA256') defaults)
    over a license payload returned by export_offline_license(), using
    only the public key from get_public_key() -- no server contact
    required, works fully offline.

    This only proves authenticity -- that `license` is byte-for-byte what
    the server's private key signed, not tampered with in transit or on
    disk -- the same scope as verifying a JWT's signature. It does NOT
    check status/expires_at/product_id; those live in `license` itself and
    are the caller's own responsibility to inspect after a True result,
    exactly as a consuming app must inspect a JWT's own claims after
    signature verification succeeds.

    Requires the `cryptography` package (not needed for the rest of this
    client): pip install cryptography
    """
    from cryptography.exceptions import InvalidSignature
    from cryptography.hazmat.primitives import hashes, serialization
    from cryptography.hazmat.primitives.asymmetric import padding

    public_key = serialization.load_pem_public_key(public_key_pem.encode('utf-8'))
    signature = base64.b64decode(signature_base64)
    # Must match the server's JSON.stringify(license) byte-for-byte: no
    # extra whitespace, and key order exactly as parsed from the response
    # (Python's json module preserves insertion order, same as JS).
    data = json.dumps(license, separators=(',', ':'), ensure_ascii=False).encode('utf-8')

    try:
        public_key.verify(signature, data, padding.PKCS1v15(), hashes.SHA256())
        return True
    except InvalidSignature:
        return False

# Usage example
def main():
    import os
    
    license_client = LicenseClient(
        os.getenv('LICENSE_KEY'),
        'https://your-license-api.com/v1'
    )
    
    if license_client.initialize():
        print("Application starting with valid license")
        
        try:
            # Simulate application work
            while True:
                license_client.record_api_call()
                license_client.record_user_activity('user123')
                time.sleep(1)
                
        except KeyboardInterrupt:
            print("\nShutdown requested...")
            license_client.shutdown()
    else:
        print("Failed to initialize license - exiting")
        exit(1)

if __name__ == "__main__":
    main()

Offline License Verification

Fetch the public key and a signed license once, then verify with no further server contact -- useful for desktop/CLI tools that need to check license authenticity without a live connection. Requires the optional cryptography package (pip install cryptography); nothing else in LicenseClient needs it.

python
license_client = LicenseClient(os.getenv('LICENSE_KEY'), 'https://your-license-api.com/v1')

public_key = license_client.get_public_key()  # fetch once, cache/embed it
license_data = license_client.export_offline_license()

if verify_signed_license(license_data['license'], license_data['signature'], public_key):
    print(f"License authentic: {license_data['license']['tier']} tier")
    # Signature verification alone doesn't check status/expiry -- inspect
    # license_data['license']['expires_at'] yourself before trusting it.
else:
    print("License signature invalid -- do not trust this file")

Flask Integration

python
from flask import Flask, request, jsonify, g
from functools import wraps

app = Flask(__name__)
license_client = None  # Initialize with your license client

class LicenseFlaskIntegration:
    def __init__(self, license_client):
        self.license_client = license_client
        
    def require_valid_license(self, f):
        """Decorator to require valid license for routes."""
        @wraps(f)
        def decorated_function(*args, **kwargs):
            try:
                validation = self.license_client.validate_license()
                if not validation.get('valid'):
                    return jsonify({
                        'error': 'Invalid license',
                        'message': 'License validation failed'
                    }), 403

                # validate_license() itself returns only {valid: bool} --
                # tier/expires_at/etc. come from the verify_license() call
                # made during initialize(), cached on the client.
                g.license_info = self.license_client.license_info
                return f(*args, **kwargs)
                
            except Exception as e:
                return jsonify({
                    'error': 'License validation error',
                    'message': str(e)
                }), 500
        return decorated_function
    
    def track_request(self):
        """Track incoming requests."""
        self.license_client.record_api_call()
        
        user_id = request.headers.get('X-User-ID')
        if user_id:
            self.license_client.record_user_activity(user_id)

# Initialize integration
license_integration = LicenseFlaskIntegration(license_client)

@app.before_request
def before_request():
    if license_client and license_client.is_activated:
        license_integration.track_request()

@app.route('/api/protected-data')
@license_integration.require_valid_license
def get_protected_data():
    return jsonify({
        'data': 'This is protected data',
        'license_tier': g.license_info.get('tier')
    })

@app.route('/api/license-status')
def license_status():
    if not license_client or not license_client.is_activated:
        return jsonify({'status': 'not_activated'})
    
    try:
        validation = license_client.validate_license()
        return jsonify({
            'status': 'active' if validation.get('valid') else 'invalid',
            'tier': license_client.license_info.get('tier'),
            'expires_at': license_client.license_info.get('expires_at')
        })
    except Exception as e:
        return jsonify({
            'status': 'error',
            'error': str(e)
        }), 500

if __name__ == '__main__':
    import os
    
    # Initialize license client
    license_client = LicenseClient(
        os.getenv('LICENSE_KEY'),
        'https://your-license-api.com/v1'
    )
    
    if license_client.initialize():
        print("Starting Flask app with license protection")
        app.run(debug=False, host='0.0.0.0', port=5000)
    else:
        print("Failed to initialize license - exiting")
        exit(1)

Java

Complete Integration Example

java
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;

public class LicenseClient {
    private final String licenseKey;
    private final String apiUrl;
    private final String instanceId;
    private final HttpClient httpClient;
    private final ObjectMapper objectMapper;
    private final ScheduledExecutorService scheduler;
    
    private boolean isActivated = false;
    private JsonNode licenseInfo;
    private UsageTracker usageTracker;
    private String token;
    
    public LicenseClient(String licenseKey, String apiUrl) {
        this.licenseKey = licenseKey;
        this.apiUrl = apiUrl.replaceAll("/$", "");
        this.instanceId = generateInstanceId();
        this.httpClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(30))
            .build();
        this.objectMapper = new ObjectMapper();
        this.scheduler = Executors.newScheduledThreadPool(2);
        this.usageTracker = new UsageTracker();
    }
    
    private String generateInstanceId() {
        try {
            String hostname = java.net.InetAddress.getLocalHost().getHostName();
            String os = System.getProperty("os.name");
            String timestamp = String.valueOf(System.currentTimeMillis());
            String combined = hostname + "-" + os + "-" + timestamp;
            
            java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(combined.getBytes());
            StringBuilder hexString = new StringBuilder();
            
            for (int i = 0; i < Math.min(8, hash.length); i++) {
                String hex = Integer.toHexString(0xff & hash[i]);
                if (hex.length() == 1) {
                    hexString.append('0');
                }
                hexString.append(hex);
            }
            
            return hexString.toString();
        } catch (Exception e) {
            return "java-instance-" + System.currentTimeMillis();
        }
    }
    
    public boolean initialize() {
        try {
            // Verify license
            JsonNode verification = verifyLicense();
            if (!verification.get("valid").asBoolean()) {
                throw new RuntimeException("Invalid license: " + 
                    verification.path("error").asText("Unknown error"));
            }
            
            // Activate license
            JsonNode activation = activateLicense();
            if (!activation.get("activated").asBoolean()) {
                throw new RuntimeException("Activation failed: " + 
                    activation.path("error").asText("Unknown error"));
            }
            
            // "token" is only present on the call that creates the
            // activation — an already-activated replay omits it, so
            // don't overwrite a previously-captured token with null.
            // It's the credential deactivateLicense() needs later.
            if (activation.hasNonNull("token")) {
                this.token = activation.get("token").asText();
            }
            this.isActivated = true;
            this.licenseInfo = verification;
            
            // Start background tasks
            startBackgroundTasks();
            
            System.out.println("License initialized successfully");
            return true;
            
        } catch (Exception e) {
            System.err.println("License initialization failed: " + e.getMessage());
            return false;
        }
    }
    
    public JsonNode verifyLicense() throws IOException, InterruptedException {
        String requestBody = objectMapper.writeValueAsString(
            java.util.Map.of("key", licenseKey)
        );
        
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(apiUrl + "/verify-license"))
            .header("Content-Type", "application/json")
            .timeout(Duration.ofSeconds(30))
            .POST(HttpRequest.BodyPublishers.ofString(requestBody))
            .build();
        
        HttpResponse<String> response = httpClient.send(request, 
            HttpResponse.BodyHandlers.ofString());
        
        return objectMapper.readTree(response.body());
    }
    
    public JsonNode activateLicense() throws IOException, InterruptedException {
        // Only key/instance_id are accepted -- the server has no
        // instance_info field.
        java.util.Map<String, Object> requestBody = java.util.Map.of(
            "key", licenseKey,
            "instance_id", instanceId
        );
        
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(apiUrl + "/activate-license"))
            .header("Content-Type", "application/json")
            .timeout(Duration.ofSeconds(30))
            .POST(HttpRequest.BodyPublishers.ofString(
                objectMapper.writeValueAsString(requestBody)))
            .build();
        
        HttpResponse<String> response = httpClient.send(request, 
            HttpResponse.BodyHandlers.ofString());
        
        return objectMapper.readTree(response.body());
    }
    
    public JsonNode validateLicense() throws IOException, InterruptedException {
        String requestBody = objectMapper.writeValueAsString(java.util.Map.of(
            "key", licenseKey,
            "instance_id", instanceId
        ));
        
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(apiUrl + "/validate-license"))
            .header("Content-Type", "application/json")
            .timeout(Duration.ofSeconds(30))
            .POST(HttpRequest.BodyPublishers.ofString(requestBody))
            .build();
        
        HttpResponse<String> response = httpClient.send(request, 
            HttpResponse.BodyHandlers.ofString());
        
        return objectMapper.readTree(response.body());
    }
    
    // Reports one metric per call -- `metric` must already be a key in
    // this license's `limits`. Requires the `token` field captured in
    // initialize() -- the license key alone is not accepted.
    public JsonNode trackUsage(String metric, int increment)
            throws IOException, InterruptedException {
        java.util.Map<String, Object> requestBody = java.util.Map.of(
            "key", licenseKey,
            "token", token,
            "metric", metric,
            "increment", increment
        );
        
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(apiUrl + "/track-usage"))
            .header("Content-Type", "application/json")
            .timeout(Duration.ofSeconds(30))
            .POST(HttpRequest.BodyPublishers.ofString(
                objectMapper.writeValueAsString(requestBody)))
            .build();
        
        HttpResponse<String> response = httpClient.send(request, 
            HttpResponse.BodyHandlers.ofString());
        
        return objectMapper.readTree(response.body());
    }

    private void startBackgroundTasks() {
        // Start validation task (every hour)
        scheduler.scheduleAtFixedRate(this::validateLicenseBackground, 
            1, 1, TimeUnit.HOURS);
        
        // Start usage tracking task (every 5 minutes)
        scheduler.scheduleAtFixedRate(this::reportUsage, 
            5, 5, TimeUnit.MINUTES);
    }
    
    private void validateLicenseBackground() {
        try {
            JsonNode validation = validateLicense();
            if (!validation.get("valid").asBoolean()) {
                System.err.println("License validation failed: " + 
                    validation.path("error").asText());
                handleInvalidLicense();
            }
        } catch (Exception e) {
            System.err.println("License validation error: " + e.getMessage());
        }
    }
    
    private void reportUsage() {
        try {
            java.util.Map<String, Integer> usage = usageTracker.getCurrentUsage();
            for (java.util.Map.Entry<String, Integer> entry : usage.entrySet()) {
                if (entry.getValue() > 0) {
                    trackUsage(entry.getKey(), entry.getValue());
                }
            }
            usageTracker.reset();
        } catch (Exception e) {
            System.err.println("Usage reporting error: " + e.getMessage());
        }
    }
    
    private void handleInvalidLicense() {
        System.err.println("License is invalid - application should shut down");
        shutdown();
    }
    
    public void recordApiCall() {
        usageTracker.recordApiCall();
    }
    
    public void recordUserActivity(String userId) {
        usageTracker.recordUserActivity(userId);
    }
    
    public void shutdown() {
        System.out.println("Shutting down license client...");
        
        if (isActivated) {
            try {
                deactivateLicense();
            } catch (Exception e) {
                System.err.println("License deactivation failed: " + e.getMessage());
            }
        }
        
        scheduler.shutdown();
        try {
            if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
                scheduler.shutdownNow();
            }
        } catch (InterruptedException e) {
            scheduler.shutdownNow();
        }
    }
    
    private void deactivateLicense() throws IOException, InterruptedException {
        String requestBody = objectMapper.writeValueAsString(java.util.Map.of(
            "key", licenseKey,
            "token", token
        ));
        
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(apiUrl + "/deactivate-license"))
            .header("Content-Type", "application/json")
            .timeout(Duration.ofSeconds(30))
            .POST(HttpRequest.BodyPublishers.ofString(requestBody))
            .build();
        
        HttpResponse<String> response = httpClient.send(request, 
            HttpResponse.BodyHandlers.ofString());
        
        JsonNode result = objectMapper.readTree(response.body());
        if (result.get("deactivated").asBoolean()) {
            System.out.println("License deactivated successfully");
        }
    }
    
    // Inner class for usage tracking. Field names map directly to the
    // license-server metric names they'll be reported under.
    private static class UsageTracker {
        private final AtomicInteger apiCallsPerDay = new AtomicInteger(0);
        private final Set<String> concurrentSessions = ConcurrentHashMap.newKeySet();
        
        public void recordApiCall() {
            apiCallsPerDay.incrementAndGet();
        }
        
        public void recordUserActivity(String userId) {
            concurrentSessions.add(userId);
        }
        
        public java.util.Map<String, Integer> getCurrentUsage() {
            java.util.Map<String, Integer> usage = new java.util.HashMap<>();
            
            usage.put("api_calls_per_day", apiCallsPerDay.get());
            usage.put("concurrent_sessions", concurrentSessions.size());
            
            return usage;
        }
        
        public void reset() {
            apiCallsPerDay.set(0);
            concurrentSessions.clear();
        }
    }
    
    // Getters
    public boolean isActivated() { return isActivated; }
    public JsonNode getLicenseInfo() { return licenseInfo; }
    public String getInstanceId() { return instanceId; }
}

// Usage example
public class Main {
    public static void main(String[] args) {
        String licenseKey = System.getenv("LICENSE_KEY");
        if (licenseKey == null) {
            System.err.println("LICENSE_KEY environment variable not set");
            System.exit(1);
        }
        
        LicenseClient licenseClient = new LicenseClient(
            licenseKey, 
            "https://your-license-api.com/v1"
        );
        
        if (licenseClient.initialize()) {
            System.out.println("Application starting with valid license");
            
            // Add shutdown hook
            Runtime.getRuntime().addShutdownHook(new Thread(licenseClient::shutdown));
            
            // Simulate application work
            try {
                while (true) {
                    licenseClient.recordApiCall();
                    licenseClient.recordUserActivity("user123");
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                System.out.println("Application interrupted");
            }
        } else {
            System.err.println("Failed to initialize license - exiting");
            System.exit(1);
        }
    }
}

Go

Complete Integration Example

go
package main

import (
    "bytes"
    "crypto/sha256"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "os/signal"
    "runtime"
    "sync"
    "sync/atomic"
    "time"
)

type LicenseClient struct {
    licenseKey   string
    apiURL       string
    instanceID   string
    token        string
    httpClient   *http.Client
    isActivated  bool
    licenseInfo  map[string]interface{}
    usageTracker *UsageTracker
    stopChan     chan struct{}
    wg           sync.WaitGroup
    mu           sync.RWMutex
}

// UsageTracker field names map directly to the license-server metric
// names they'll be reported under.
type UsageTracker struct {
    apiCallsPerDay    int64
    concurrentSessions sync.Map
    mu                sync.Mutex
}

func NewLicenseClient(licenseKey, apiURL string) *LicenseClient {
    return &LicenseClient{
        licenseKey:   licenseKey,
        apiURL:       apiURL,
        instanceID:   generateInstanceID(),
        httpClient:   &http.Client{Timeout: 30 * time.Second},
        usageTracker: &UsageTracker{},
        stopChan:     make(chan struct{}),
    }
}

func generateInstanceID() string {
    hostname, _ := os.Hostname()
    goos := runtime.GOOS
    timestamp := fmt.Sprintf("%d", time.Now().UnixNano())
    
    combined := fmt.Sprintf("%s-%s-%s", hostname, goos, timestamp)
    hash := sha256.Sum256([]byte(combined))
    return fmt.Sprintf("%x", hash[:8])
}

func (lc *LicenseClient) Initialize() error {
    // Verify license
    verification, err := lc.verifyLicense()
    if err != nil {
        return fmt.Errorf("license verification failed: %w", err)
    }
    
    valid, ok := verification["valid"].(bool)
    if !ok || !valid {
        errorMsg := "unknown error"
        if errStr, ok := verification["error"].(string); ok {
            errorMsg = errStr
        }
        return fmt.Errorf("invalid license: %s", errorMsg)
    }
    
    // Activate license
    activation, err := lc.activateLicense()
    if err != nil {
        return fmt.Errorf("license activation failed: %w", err)
    }
    
    activated, ok := activation["activated"].(bool)
    if !ok || !activated {
        errorMsg := "unknown error"
        if errStr, ok := activation["error"].(string); ok {
            errorMsg = errStr
        }
        return fmt.Errorf("activation failed: %s", errorMsg)
    }
    
    // "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 an empty string. It's
    // the credential deactivateLicense() needs later.
    lc.mu.Lock()
    if token, ok := activation["token"].(string); ok && token != "" {
        lc.token = token
    }
    lc.isActivated = true
    lc.licenseInfo = verification
    lc.mu.Unlock()
    
    // Start background tasks
    lc.startBackgroundTasks()
    
    fmt.Println("License initialized successfully")
    return nil
}

func (lc *LicenseClient) verifyLicense() (map[string]interface{}, error) {
    payload := map[string]interface{}{
        "key": lc.licenseKey,
    }
    
    return lc.makeRequest("POST", "/verify-license", payload)
}

func (lc *LicenseClient) activateLicense() (map[string]interface{}, error) {
    // Only key/instance_id are accepted -- the server has no
    // instance_info field.
    payload := map[string]interface{}{
        "key":         lc.licenseKey,
        "instance_id": lc.instanceID,
    }
    
    return lc.makeRequest("POST", "/activate-license", payload)
}

func (lc *LicenseClient) validateLicense() (map[string]interface{}, error) {
    payload := map[string]interface{}{
        "key":         lc.licenseKey,
        "instance_id": lc.instanceID,
    }
    
    return lc.makeRequest("POST", "/validate-license", payload)
}

// trackUsage reports one metric per call -- metric must already be a key
// in this license's `limits`. Requires lc.token (the activation token
// captured in Initialize()) -- the license key alone is not accepted.
func (lc *LicenseClient) trackUsage(metric string, increment int) (map[string]interface{}, error) {
    lc.mu.RLock()
    token := lc.token
    lc.mu.RUnlock()

    payload := map[string]interface{}{
        "key":       lc.licenseKey,
        "token":     token,
        "metric":    metric,
        "increment": increment,
    }
    
    return lc.makeRequest("POST", "/track-usage", payload)
}

func (lc *LicenseClient) makeRequest(method, endpoint string, payload map[string]interface{}) (map[string]interface{}, error) {
    jsonData, err := json.Marshal(payload)
    if err != nil {
        return nil, err
    }
    
    url := lc.apiURL + endpoint
    req, err := http.NewRequest(method, url, bytes.NewBuffer(jsonData))
    if err != nil {
        return nil, err
    }
    
    req.Header.Set("Content-Type", "application/json")
    
    resp, err := lc.httpClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }
    
    var result map[string]interface{}
    if err := json.Unmarshal(body, &result); err != nil {
        return nil, err
    }
    
    return result, nil
}

func (lc *LicenseClient) startBackgroundTasks() {
    // Start validation goroutine
    lc.wg.Add(1)
    go lc.validationLoop()
    
    // Start usage tracking goroutine
    lc.wg.Add(1)
    go lc.usageTrackingLoop()
}

func (lc *LicenseClient) validationLoop() {
    defer lc.wg.Done()
    
    ticker := time.NewTicker(1 * time.Hour)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            validation, err := lc.validateLicense()
            if err != nil {
                fmt.Printf("License validation error: %v\n", err)
                continue
            }
            
            valid, ok := validation["valid"].(bool)
            if !ok || !valid {
                // validate-license always returns just {"valid": false} on
                // failure -- no error field to read a reason from.
                fmt.Printf("License validation failed\n")
                lc.handleInvalidLicense()
                return
            }
            
        case <-lc.stopChan:
            return
        }
    }
}

func (lc *LicenseClient) usageTrackingLoop() {
    defer lc.wg.Done()
    
    ticker := time.NewTicker(5 * time.Minute)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            usage := lc.usageTracker.getCurrentUsage()
            for metric, increment := range usage {
                if increment > 0 {
                    if _, err := lc.trackUsage(metric, increment); err != nil {
                        fmt.Printf("Usage tracking error (%s): %v\n", metric, err)
                    }
                }
            }
            lc.usageTracker.reset()
            
        case <-lc.stopChan:
            return
        }
    }
}

func (lc *LicenseClient) handleInvalidLicense() {
    fmt.Println("License is invalid - application should shut down")
    lc.Shutdown()
}

func (lc *LicenseClient) RecordAPICall() {
    atomic.AddInt64(&lc.usageTracker.apiCallsPerDay, 1)
}

func (lc *LicenseClient) RecordUserActivity(userID string) {
    lc.usageTracker.concurrentSessions.Store(userID, time.Now())
}

func (lc *LicenseClient) Shutdown() {
    fmt.Println("Shutting down license client...")
    
    lc.mu.RLock()
    isActivated := lc.isActivated
    lc.mu.RUnlock()
    
    if isActivated {
        if err := lc.deactivateLicense(); err != nil {
            fmt.Printf("License deactivation failed: %v\n", err)
        }
    }
    
    close(lc.stopChan)
    lc.wg.Wait()
}

func (lc *LicenseClient) deactivateLicense() error {
    payload := map[string]interface{}{
        "key":   lc.licenseKey,
        "token": lc.token,
    }
    
    result, err := lc.makeRequest("POST", "/deactivate-license", payload)
    if err != nil {
        return err
    }
    
    deactivated, ok := result["deactivated"].(bool)
    if ok && deactivated {
        fmt.Println("License deactivated successfully")
    }
    
    return nil
}

// UsageTracker methods
func (ut *UsageTracker) getCurrentUsage() map[string]int {
    ut.mu.Lock()
    defer ut.mu.Unlock()
    
    // Count concurrent sessions
    sessionCount := 0
    ut.concurrentSessions.Range(func(key, value interface{}) bool {
        sessionCount++
        return true
    })
    
    usage := map[string]int{
        "api_calls_per_day":  int(atomic.LoadInt64(&ut.apiCallsPerDay)),
        "concurrent_sessions": sessionCount,
    }
    
    return usage
}

func (ut *UsageTracker) reset() {
    atomic.StoreInt64(&ut.apiCallsPerDay, 0)
    ut.concurrentSessions = sync.Map{} // Create new map to clear old sessions
}

// Getters
func (lc *LicenseClient) IsActivated() bool {
    lc.mu.RLock()
    defer lc.mu.RUnlock()
    return lc.isActivated
}

func (lc *LicenseClient) GetLicenseInfo() map[string]interface{} {
    lc.mu.RLock()
    defer lc.mu.RUnlock()
    return lc.licenseInfo
}

func (lc *LicenseClient) GetInstanceID() string {
    return lc.instanceID
}

// Main usage example
func main() {
    licenseKey := os.Getenv("LICENSE_KEY")
    if licenseKey == "" {
        fmt.Println("LICENSE_KEY environment variable not set")
        os.Exit(1)
    }
    
    licenseClient := NewLicenseClient(licenseKey, "https://your-license-api.com/v1")
    
    if err := licenseClient.Initialize(); err != nil {
        fmt.Printf("Failed to initialize license: %v\n", err)
        os.Exit(1)
    }
    
    fmt.Println("Application starting with valid license")
    
    // Simulate application work
    go func() {
        for {
            licenseClient.RecordAPICall()
            licenseClient.RecordUserActivity("user123")
            time.Sleep(1 * time.Second)
        }
    }()
    
    // Wait for interrupt signal
    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt)
    <-c
    
    fmt.Println("\nShutdown requested...")
    licenseClient.Shutdown()
}

For general integration best practices (error handling, performance, security, monitoring), see the Integration Checklist.

Auto-Update Checking

This documents the raw POST /v1/check-update contract only — see Software Distribution for the full picture. Framework-specific integration guides (Tauri, electron-updater, Squirrel) are planned as a fast-follow and not covered here yet.

Request:

bash
curl -X POST https://your-license-api.com/v1/check-update \
  -H "Content-Type: application/json" \
  -d '{
    "key": "your-customers-license-key",
    "product_id": "widget-pro",
    "channel": "stable",
    "platform": "darwin-arm64",
    "current_version": "2.2.9"
  }'

Response (update_available: false, release: null when nothing published matches, or when current_version is already current):

json
{
  "update_available": true,
  "release": {
    "product_id": "widget-pro",
    "version": "2.3.0",
    "channel": "stable",
    "platform": "darwin-arm64",
    "artifact_url": "https://cdn.example.com/widget-pro/2.3.0/widget-pro-mac.dmg",
    "checksum": "sha256:...",
    "release_notes": "Fixes a rare crash on startup.",
    "signature": "base64-RSA-SHA256-signature...",
    "issued_at": "2026-08-19T00:00:00.000Z"
  }
}

Field order matters when verifying signature: the server signs the plain JSON.stringify() of the manifest with no canonical/sorted-key serialization, so your client must reconstruct the object in the exact order shown above (with signature itself excluded) before stringifying it — see the API reference for the full explanation. This mainly bites non-JavaScript clients: a JSON encoder that sorts keys (Python's json.dumps(sort_keys=True), Go's encoding/json on a map) will produce a mismatch even for an untampered response.

Node.js: checking for an update and verifying the signature

Same offline-verification scheme as an exported license file (see the Python verify_signed_license example above) — signature is an RSA/SHA256 signature over the release object with signature itself removed, verified against the public key from GET /v1/public-key.

javascript
const crypto = require('crypto');

async function checkForUpdate(apiUrl, licenseKey, productId, currentVersion) {
  const res = await fetch(`${apiUrl}/check-update`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      key: licenseKey,
      product_id: productId,
      channel: 'stable',
      platform: `${process.platform}-${process.arch}`,
      current_version: currentVersion,
    }),
  });
  if (!res.ok) {
    throw new Error(`check-update failed: ${res.status}`);
  }
  return res.json();
}

async function verifyRelease(apiUrl, release) {
  const { signature, ...manifest } = release;

  // Fetching the public key here, on every check, only proves the
  // manifest matches whatever key the server hands back right now - it
  // adds nothing beyond what TLS already gives you against a
  // compromised or malicious server. The real offline-verification
  // value of this signature only shows up if you cache/pin the public
  // key out of band (embed it in your build, or fetch and store it once)
  // instead of re-fetching it alongside every manifest.
  const keyRes = await fetch(`${apiUrl}/public-key`);
  const { publicKey } = await keyRes.json();

  const verifier = crypto.createVerify('SHA256');
  verifier.update(JSON.stringify(manifest));
  verifier.end();
  return verifier.verify(publicKey, signature, 'base64');
}

// Usage
const { update_available, release } = await checkForUpdate(
  'https://your-license-api.com/v1',
  process.env.LICENSE_KEY,
  'widget-pro',
  APP_VERSION
);

if (update_available) {
  const authentic = await verifyRelease('https://your-license-api.com/v1', release);
  if (!authentic) {
    throw new Error('Update signature invalid -- refusing to trust this release');
  }

  // A valid signature only proves the manifest wasn't tampered with, not
  // that it's current -- unpublishing a release doesn't retroactively
  // invalidate a signature you already have. Only relevant if you cache
  // this response instead of always calling check-update fresh; skip
  // this check if you always act on a live response immediately.
  const MAX_MANIFEST_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours, tune to your own risk tolerance
  if (Date.now() - new Date(release.issued_at).getTime() > MAX_MANIFEST_AGE_MS) {
    throw new Error('Cached update manifest is too old -- re-check with the server before trusting it');
  }

  console.log(`Update available: ${release.version}`, release.artifact_url);
  // Fetch release.artifact_url yourself and confirm it hashes to
  // release.checksum before running/installing anything from it.

  // release.product_id echoes back exactly how it was typed at
  // registration time, not how you typed it in this request - "WidgetPro"
  // and "widgetpro" are treated as the same product server-side, but the
  // signed manifest still carries whichever casing was first registered.
  // If you add your own strict `release.product_id === MY_PRODUCT_ID`
  // check on top of this, normalize both sides first (lowercase/trim) or
  // it can reject a perfectly valid response.
}

Next Steps