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
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
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
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.
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
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
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
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:
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):
{
"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.
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
- Error Handling - Comprehensive error management strategies
- API Reference - Complete endpoint documentation
- Getting Started - Set up your License Server
