License Verification
Detailed guide on implementing license verification in your applications, including caching strategies, offline support, and best practices.
Basic License Verification
Simple Verification
The most basic license verification checks if a key is valid:
curl -X POST https://your-license-api.com/v1/verify-license \
-H "Content-Type: application/json" \
-d '{
"key": "7yChyZcfMG23Dx1sjBoLziPFrH4n-6f28"
}'Response:
{
"valid": true,
"tier": "pro",
"product_id": "your-product",
"limits": {
"users": 100,
"api_calls_per_day": 10000
},
"expires_at": "2025-12-31T23:59:59Z",
"issued_at": "2025-01-01T00:00:00Z",
"status": "active"
}Verification with Instance Tracking
For applications that need activation tracking:
curl -X POST https://your-license-api.com/v1/validate-license \
-H "Content-Type: application/json" \
-d '{
"key": "7yChyZcfMG23Dx1sjBoLziPFrH4n-6f28",
"instance_id": "server-001"
}'Implementation Patterns
1. Startup Verification
Verify license when your application starts:
class ApplicationBootstrap {
constructor(licenseKey, apiUrl) {
this.licenseKey = licenseKey;
this.apiUrl = apiUrl;
this.licenseInfo = null;
}
async initialize() {
try {
// Verify license before starting application
const verification = await this.verifyLicense();
if (!verification.valid) {
throw new Error(`Invalid license: ${verification.error}`);
}
this.licenseInfo = verification;
// Note: verify-license deliberately omits issued_to from its
// response (a privacy choice) - it's always undefined here. Use
// GET /v1/admin/license/:key (admin-authenticated) if you need it.
console.log(`License tier: ${verification.tier}`);
// Start application with license constraints
this.startApplication(verification);
} catch (error) {
console.error('License verification failed:', error.message);
this.showLicenseError(error);
}
}
async verifyLicense() {
const response = await fetch(`${this.apiUrl}/verify-license`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: this.licenseKey })
});
return response.json();
}
startApplication(licenseInfo) {
// Apply license limits to application features
if (licenseInfo.limits.users) {
this.setMaxUsers(licenseInfo.limits.users);
}
if (licenseInfo.limits.api_calls_per_day) {
this.setAPICallLimit(licenseInfo.limits.api_calls_per_day);
}
console.log('Application started successfully');
}
}
// Usage
const app = new ApplicationBootstrap(
process.env.LICENSE_KEY,
'https://your-license-api.com/v1'
);
app.initialize();2. Periodic Verification
Continuously verify license validity during runtime:
class PeriodicLicenseChecker {
constructor(licenseKey, apiUrl, intervalMinutes = 60) {
this.licenseKey = licenseKey;
this.apiUrl = apiUrl;
this.interval = intervalMinutes * 60 * 1000;
this.isRunning = false;
this.intervalId = null;
this.lastVerification = null;
}
start() {
if (this.isRunning) return;
this.isRunning = true;
console.log(`Starting periodic license verification every ${this.interval / 60000} minutes`);
// Initial check
this.checkLicense();
// Schedule periodic checks
this.intervalId = setInterval(() => {
this.checkLicense();
}, this.interval);
}
stop() {
if (!this.isRunning) return;
this.isRunning = false;
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
console.log('Stopped periodic license verification');
}
async checkLicense() {
try {
const verification = await this.verifyLicense();
this.lastVerification = {
timestamp: new Date(),
result: verification
};
if (!verification.valid) {
console.error('License verification failed:', verification.error);
this.handleInvalidLicense(verification);
} else {
console.log('License verification successful');
this.handleValidLicense(verification);
}
} catch (error) {
console.error('License verification error:', error.message);
this.handleVerificationError(error);
}
}
async verifyLicense() {
const response = await fetch(`${this.apiUrl}/verify-license`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: this.licenseKey })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
handleValidLicense(verification) {
// Check if license is expiring soon
if (verification.expires_at) {
const expiryDate = new Date(verification.expires_at);
const daysUntilExpiry = (expiryDate - new Date()) / (1000 * 60 * 60 * 24);
if (daysUntilExpiry <= 30) {
console.warn(`License expires in ${Math.floor(daysUntilExpiry)} days`);
this.notifyExpiryWarning(daysUntilExpiry);
}
}
}
handleInvalidLicense(verification) {
// The API has no `expired`/`revoked` boolean fields -- an invalid
// license is always just {valid: false, error: "<message>"}, so branch
// on the message text instead. Note the server doesn't distinguish
// "revoked" from other non-active states in its message -- a revoked
// license and, say, a suspended one both come back as "License is not
// active", so this can't reliably tell revoked apart from other cases.
if (verification.error?.includes('expired')) {
this.handleExpiredLicense();
} else if (verification.error?.includes('not active')) {
this.handleRevokedLicense();
} else {
this.handleGenericInvalidLicense();
}
}
handleVerificationError(error) {
// Network or server errors - don't immediately shut down
const gracePeriodHours = 4;
const lastSuccess = this.getLastSuccessfulVerification();
if (lastSuccess && (Date.now() - lastSuccess) < gracePeriodHours * 60 * 60 * 1000) {
console.warn(`License verification failed, but within grace period. Continuing operation.`);
} else {
console.error('Grace period exceeded. Application should shut down.');
this.initiateGracefulShutdown();
}
}
handleExpiredLicense() {
console.error('License has expired');
this.showExpiryDialog();
// Optionally shut down application
}
handleRevokedLicense() {
console.error('License has been revoked');
this.initiateGracefulShutdown();
}
handleGenericInvalidLicense() {
console.error('License is invalid');
this.showLicenseErrorDialog();
}
getLastSuccessfulVerification() {
return this.lastVerification?.result?.valid ?
this.lastVerification.timestamp.getTime() : null;
}
notifyExpiryWarning(daysRemaining) {
// Show user notification about upcoming expiry
console.warn(`License expires in ${Math.floor(daysRemaining)} days`);
}
showExpiryDialog() {
// Show expiry dialog to user
console.log('Please renew your license');
}
showLicenseErrorDialog() {
// Show license error dialog
console.log('License error - please contact support');
}
initiateGracefulShutdown() {
console.log('Initiating graceful shutdown due to license issues');
// Implement graceful shutdown logic
process.exit(1);
}
}
// Usage
const licenseChecker = new PeriodicLicenseChecker(
process.env.LICENSE_KEY,
'https://your-license-api.com/v1',
60 // Check every hour
);
licenseChecker.start();
// Graceful shutdown
process.on('SIGTERM', () => {
licenseChecker.stop();
});Caching Strategies
1. In-Memory Caching
Cache verification results to reduce API calls:
class CachedLicenseVerifier {
constructor(apiUrl, cacheMinutes = 30) {
this.apiUrl = apiUrl;
this.cache = new Map();
this.cacheTTL = cacheMinutes * 60 * 1000;
}
async verifyLicense(key) {
// Check cache first
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
console.log('Returning cached license verification');
return { ...cached.result, fromCache: true };
}
try {
// Verify with API
const result = await this.verifyWithAPI(key);
// Cache successful results
if (result.valid) {
this.cache.set(key, {
result,
timestamp: Date.now()
});
}
return result;
} catch (error) {
// If API fails, return cached result if available
if (cached) {
console.warn('API verification failed, using cached result');
return { ...cached.result, fromCache: true, warning: 'Offline verification' };
}
throw error;
}
}
async verifyWithAPI(key) {
const response = await fetch(`${this.apiUrl}/verify-license`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key })
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
}
clearCache(key = null) {
if (key) {
this.cache.delete(key);
} else {
this.cache.clear();
}
}
getCacheStats() {
const now = Date.now();
let valid = 0, expired = 0;
for (const [key, cached] of this.cache.entries()) {
if (now - cached.timestamp < this.cacheTTL) {
valid++;
} else {
expired++;
}
}
return { total: this.cache.size, valid, expired };
}
}2. Persistent Caching
Store verification results on disk for offline support:
const fs = require('fs').promises;
const path = require('path');
class PersistentLicenseCache {
constructor(cacheDir = './license-cache') {
this.cacheDir = cacheDir;
this.ensureCacheDir();
}
async ensureCacheDir() {
try {
await fs.mkdir(this.cacheDir, { recursive: true });
} catch (error) {
console.error('Failed to create cache directory:', error);
}
}
getCacheFilePath(key) {
const hasher = require('crypto').createHash('sha256');
hasher.update(key);
const keyHash = hasher.digest('hex').substring(0, 16);
return path.join(this.cacheDir, `license-${keyHash}.json`);
}
async getCachedVerification(key, maxAgeMinutes = 60) {
try {
const filePath = this.getCacheFilePath(key);
const data = await fs.readFile(filePath, 'utf8');
const cached = JSON.parse(data);
const age = Date.now() - cached.timestamp;
const maxAge = maxAgeMinutes * 60 * 1000;
if (age < maxAge) {
return { ...cached.result, fromCache: true };
} else {
// Cache expired
await this.removeCachedVerification(key);
return null;
}
} catch (error) {
// Cache file doesn't exist or is corrupted
return null;
}
}
async setCachedVerification(key, result) {
try {
const filePath = this.getCacheFilePath(key);
const cached = {
result,
timestamp: Date.now(),
key: key.substring(0, 8) + '...' // Partial key for identification
};
await fs.writeFile(filePath, JSON.stringify(cached, null, 2));
} catch (error) {
console.error('Failed to cache verification:', error);
}
}
async removeCachedVerification(key) {
try {
const filePath = this.getCacheFilePath(key);
await fs.unlink(filePath);
} catch (error) {
// File might not exist, ignore error
}
}
async clearAllCache() {
try {
const files = await fs.readdir(this.cacheDir);
const licenseFiles = files.filter(f => f.startsWith('license-'));
await Promise.all(
licenseFiles.map(file =>
fs.unlink(path.join(this.cacheDir, file))
)
);
console.log(`Cleared ${licenseFiles.length} cached license files`);
} catch (error) {
console.error('Failed to clear cache:', error);
}
}
async getCacheInfo() {
try {
const files = await fs.readdir(this.cacheDir);
const licenseFiles = files.filter(f => f.startsWith('license-'));
const cacheInfo = [];
for (const file of licenseFiles) {
try {
const filePath = path.join(this.cacheDir, file);
const data = await fs.readFile(filePath, 'utf8');
const cached = JSON.parse(data);
cacheInfo.push({
file,
key: cached.key,
timestamp: new Date(cached.timestamp),
age: Date.now() - cached.timestamp,
valid: cached.result.valid
});
} catch (error) {
console.warn(`Corrupted cache file: ${file}`);
}
}
return cacheInfo;
} catch (error) {
console.error('Failed to get cache info:', error);
return [];
}
}
}
// Combined verifier with persistent cache
class RobustLicenseVerifier {
constructor(apiUrl) {
this.apiUrl = apiUrl;
this.cache = new PersistentLicenseCache();
}
async verifyLicense(key, options = {}) {
const {
useCache = true,
cacheMinutes = 60,
offlineMode = false
} = options;
// Try cache first if enabled
if (useCache) {
const cached = await this.cache.getCachedVerification(key, cacheMinutes);
if (cached) {
console.log('Using cached license verification');
return cached;
}
}
// If offline mode, return failure if no cache
if (offlineMode) {
return {
valid: false,
error: 'Offline mode and no cached verification available'
};
}
try {
// Verify online
const result = await this.verifyOnline(key);
// Cache successful verifications
if (result.valid && useCache) {
await this.cache.setCachedVerification(key, result);
}
return result;
} catch (error) {
// API failed, try to use cached result
if (useCache) {
const cached = await this.cache.getCachedVerification(key, cacheMinutes * 10); // Extended grace period
if (cached) {
console.warn('API verification failed, using cached result');
return {
...cached,
warning: 'Verification failed, using cached result'
};
}
}
throw error;
}
}
async verifyOnline(key) {
const response = await fetch(`${this.apiUrl}/verify-license`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
}Cryptographic Offline Verification
The caching strategies above are about resilience — surviving a temporary network outage using a previous verification result. This section covers something different: a license file that can be cryptographically verified with no server contact at all, not even once the network is back, using RSA public-key signatures.
This is distinct from GET /v1/export-license/:key (HMAC, shared secret) and POST /v1/verify-license-file / verify-license-file-base64 (which still check the database for revocation on every call). The offline-RSA path trades that live revocation check for true air-gapped verification.
Fetching the Public Key
Fetch this once and embed or cache it in your application — it isn't meant to be re-fetched before every verification:
curl https://your-license-api.com/v1/public-key{ "publicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n" }Fetching a Signed License
curl https://your-license-api.com/v1/export-license/7yChyZcfMG23Dx1sjBoLziPFrH4n-6f28/offline{
"license": {
"key": "7yChyZcfMG23Dx1sjBoLziPFrH4n-6f28",
"tier": "pro",
"product_id": "my-product",
"expires_at": "2024-12-31T23:59:59.000Z",
"limits": {}
},
"signature": "base64-encoded-rsa-signature"
}Verifying Fully Offline
import { createVerify } from 'node:crypto';
function verifyOfflineLicense(license, signature, publicKey) {
const verifier = createVerify('SHA256');
verifier.update(JSON.stringify(license));
verifier.end();
return verifier.verify(publicKey, signature, 'base64');
}This confirms the license payload hasn't been tampered with since it was exported, using only the cached public key — no server round-trip. What it does not know is whether the license has been revoked since export. If live revocation status matters for your product, use verify-license-file/verify-license-file-base64 instead, or re-fetch the offline export periodically.
Feature Gating Based on License
License-Based Feature Control
class FeatureManager {
constructor(licenseInfo) {
this.licenseInfo = licenseInfo;
this.features = new Map();
this.initializeFeatures();
}
initializeFeatures() {
const tier = this.licenseInfo.tier;
const limits = this.licenseInfo.limits || {};
// Define features based on license tier
this.features.set('basic_features', true);
this.features.set('advanced_features', ['pro', 'enterprise'].includes(tier));
this.features.set('premium_features', tier === 'enterprise');
this.features.set('api_access', true);
this.features.set('bulk_operations', tier !== 'basic');
this.features.set('custom_integrations', tier === 'enterprise');
// Set usage limits
this.setUsageLimits(limits);
}
setUsageLimits(limits) {
// Keys here must be from the server's fixed allowlist:
// users, seats, admins, projects, environments, tenants,
// api_calls_per_day, rate_limit_rps, concurrent_sessions, features.
// There is no storage-related limit key.
this.limits = {
maxUsers: limits.users || Infinity,
maxAPICallsPerDay: limits.api_calls_per_day || Infinity,
maxConcurrentSessions: limits.concurrent_sessions || Infinity,
maxProjects: limits.projects || Infinity
};
}
isFeatureEnabled(featureName) {
return this.features.get(featureName) || false;
}
checkUsageLimit(limitType, currentUsage) {
const limit = this.limits[limitType];
if (limit === Infinity) return { allowed: true };
const allowed = currentUsage < limit;
const remaining = Math.max(0, limit - currentUsage);
const percentage = (currentUsage / limit) * 100;
return {
allowed,
limit,
current: currentUsage,
remaining,
percentage
};
}
getFeatureList() {
return Array.from(this.features.entries()).map(([name, enabled]) => ({
name,
enabled
}));
}
getLimits() {
return { ...this.limits };
}
}
// Usage in application
class MyApplication {
constructor(licenseKey) {
this.licenseKey = licenseKey;
this.featureManager = null;
}
async initialize() {
// Verify license
const verifier = new RobustLicenseVerifier('https://your-license-api.com/v1');
const licenseInfo = await verifier.verifyLicense(this.licenseKey);
if (!licenseInfo.valid) {
throw new Error('Invalid license');
}
// Initialize feature management
this.featureManager = new FeatureManager(licenseInfo);
console.log('Available features:', this.featureManager.getFeatureList());
console.log('Usage limits:', this.featureManager.getLimits());
}
async createProject(projectData) {
// Check if feature is enabled
if (!this.featureManager.isFeatureEnabled('basic_features')) {
throw new Error('Project creation not available in your license');
}
// Check usage limits
const currentProjects = await this.getCurrentProjectCount();
const limitCheck = this.featureManager.checkUsageLimit('maxProjects', currentProjects);
if (!limitCheck.allowed) {
throw new Error(`Project limit reached: ${limitCheck.current}/${limitCheck.limit}`);
}
// Create project
return this.doCreateProject(projectData);
}
async makeAPICall() {
const currentCalls = await this.getCurrentAPICallCount();
const limitCheck = this.featureManager.checkUsageLimit('maxAPICallsPerDay', currentCalls);
if (!limitCheck.allowed) {
throw new Error(`API call limit reached: ${limitCheck.current}/${limitCheck.limit}`);
}
if (limitCheck.percentage > 80) {
console.warn(`API usage warning: ${limitCheck.percentage.toFixed(1)}% of limit used`);
}
return this.doMakeAPICall();
}
// Placeholder methods
async getCurrentProjectCount() { return 0; }
async getCurrentAPICallCount() { return 0; }
async doCreateProject(data) { return { id: 1, ...data }; }
async doMakeAPICall() { return { success: true }; }
}For general integration best practices (error handling, performance, security, monitoring), see the Integration Checklist.
Next Steps
- License Activation - Track instance activations
- Usage Tracking - Monitor license usage
- Client Examples - Implementation examples in various languages
