Build Licensed Software
in Minutes
ScaleGate provides a complete licensing infrastructure for desktop and web applications. Integrate in 5 lines of code.
Bulletproof Licensing
Hardware-locked activations, offline validation, and automatic revalidation keep your software protected.
5 Lines of Code
From zero to fully licensed in minutes. Our SDKs handle activation, validation, renewals, and updates.
Desktop & Web
.NET SDK for WPF/WinForms desktop apps. JavaScript SDK for React, Next.js, Vue, and vanilla web apps.
➣ How It Works
Four simple steps from signup to selling licensed software.
Create Your App
Sign in to the ScaleGate dashboard and create a new application. You'll get an App ID and API keys.
Install the SDK
Add the .NET or Web SDK to your project with a single package manager command.
Add 5 Lines
Initialize the client, pass a license key, and call activate. That's it-your app is now licensed.
Start Selling
Issue license keys from your dashboard. Customers activate, and you track everything in real time.
💻 .NET SDK
The .NET SDK is designed for desktop applications (WPF, WinForms, MAUI) targeting .NET Standard 2.0+.
Installation
Install via NuGet Package Manager or the .NET CLI:
dotnet add package ScaleGate.Licensing.SDK
Install-Package ScaleGate.Licensing.SDK
<PackageReference Include="ScaleGate.Licensing.SDK" Version="3.*" />
Quick Start
Here is a complete working example you can paste directly into a WPF or WinForms application:
using ScaleGate.Licensing.SDK;
// In your App.xaml.cs or Program.cs
try
{
var client = await LicenseClient
.Create("YOUR_APP_ID", "XXXX-XXXX-XXXX-XXXX")
.WithApiUrl("https://your-tenant.scalegate.io")
.WithOfflineValidation(true)
.WithGracePeriod(days: 7)
.WithAutoRevalidation(intervalMinutes: 30)
.BuildAndActivateAsync();
if (client.Status == LicenseStatus.Active)
{
// License is valid - launch your app
Console.WriteLine($"Licensed to: {client.LicenseInfo.CustomerName}");
Console.WriteLine($"Expires: {client.LicenseInfo.ExpiresAt:d}");
}
}
catch (LicenseActivationException ex)
{
// Handle activation failure
MessageBox.Show($"Activation failed: {ex.Message}");
}
catch (LicenseExpiredException ex)
{
// Prompt user to renew
MessageBox.Show($"License expired on {ex.ExpiredAt:d}. Please renew.");
}
Configuration Reference
Every option available on the LicenseClient builder:
| Method | Type | Default | Description |
|---|---|---|---|
.Create(appId, licenseKey) | string, string | Required | Your application ID and the customer's license key |
.WithApiUrl(url) | string | Required | Your ScaleGate tenant URL (e.g. https://acme.scalegate.io) |
.WithOfflineValidation(enabled) | bool | false | Cache license data locally for offline use |
.WithGracePeriod(days) | int | 0 | Days the app can run offline before blocking |
.WithAutoRevalidation(intervalMinutes) | int | 0 (off) | Periodic background re-check interval in minutes |
.WithDeviceLimit(limit) | int | 1 | Max concurrent devices per license key |
.WithProxy(host, port) | string, int | none | Route API calls through an HTTP proxy |
.WithTimeout(seconds) | int | 30 | HTTP request timeout in seconds |
.WithRetry(maxAttempts, delayMs) | int, int | 3, 1000 | Retry policy for transient network errors |
.WithLogger(logger) | ILogger | none | Pass your own logger for SDK diagnostic output |
.OnStatusChanged(handler) | Action<LicenseStatusChangedArgs> | none | Register a callback for license status changes |
.BuildAndActivateAsync() | - | - | Build the client and attempt activation |
.BuildAsync() | - | - | Build the client without auto-activating |
License Lifecycle
Activate
Activation binds a license key to the current device using a hardware-based fingerprint. Each key has a maximum device count set in the dashboard.
var result = await client.ActivateAsync();
switch (result.Status)
{
case LicenseStatus.Active:
// Activation succeeded
Console.WriteLine($"Activated for {result.CustomerName}");
break;
case LicenseStatus.DeviceLimitReached:
// User has too many devices
Console.WriteLine("Deactivate another device first.");
break;
case LicenseStatus.Expired:
// Key has expired
Console.WriteLine($"Expired on {result.ExpiresAt:d}");
break;
case LicenseStatus.Revoked:
// Key was revoked by admin
Console.WriteLine("This license has been revoked.");
break;
case LicenseStatus.Invalid:
// Key not found or malformed
Console.WriteLine("Invalid license key.");
break;
}
| Property | Type | Description |
|---|---|---|
Status | LicenseStatus | The resulting license status |
CustomerName | string | Registered customer name |
CustomerEmail | string | Registered customer email |
ExpiresAt | DateTime? | License expiration date (null for perpetual) |
Features | string[] | Array of enabled feature flags |
MaxDevices | int | Maximum allowed devices |
ActiveDevices | int | Current active device count |
Message | string | Human-readable status message |
Validate
Validates that the current device's activation is still valid. Call this on app startup or periodically.
var validation = await client.ValidateAsync();
if (validation.IsValid)
{
// Good to go
Console.WriteLine($"Valid until {validation.ExpiresAt:d}");
Console.WriteLine($"Features: {string.Join(", ", validation.Features)}");
}
else
{
// Check validation.Status for the specific reason
HandleLicenseFailure(validation.Status, validation.Message);
}
Renew
Renew an expired or expiring license key. Returns the updated expiration date on success.
var renewal = await client.RenewAsync();
if (renewal.Success)
{
Console.WriteLine($"Renewed! New expiry: {renewal.ExpiresAt:d}");
}
else
{
Console.WriteLine($"Renewal failed: {renewal.Message}");
}
Check Status
Lightweight status check without full validation. Useful for UI indicators.
var status = client.Status; // LicenseStatus enum (cached)
var info = client.LicenseInfo; // Full license details
statusLabel.Text = status switch
{
LicenseStatus.Active => "Licensed",
LicenseStatus.GracePeriod => $"Offline grace ({info.GraceDaysRemaining}d left)",
LicenseStatus.Expired => "Expired - please renew",
_ => "Unlicensed"
};
Status Reference
| Status | Meaning | Recommended Action |
|---|---|---|
| Active | License is valid and device is authorized | Allow full access |
| GracePeriod | Offline but within grace window | Show warning, allow access |
| Expired | License has passed its expiration date | Prompt renewal, limit features |
| Suspended | Temporarily suspended by admin | Show message, block access |
| Revoked | Permanently revoked by admin | Block access, contact support |
| Invalid | Key not found, malformed, or wrong app | Prompt re-entry of key |
| DeviceLimitReached | Max concurrent device activations exceeded | Deactivate another device |
| Trial | In trial period | Show trial countdown, prompt upgrade |
| PendingActivation | Key exists but not yet activated on this device | Call ActivateAsync() |
Offline Mode
When .WithOfflineValidation(true) is enabled, the SDK caches a cryptographically signed license token on the local device. If the device goes offline, the SDK falls back to this cached token.
The .WithGracePeriod(days) controls how long the app can run offline before requiring an online check. Once the grace period expires, the license status changes to Expired.
var client = await LicenseClient
.Create("YOUR_APP_ID", licenseKey)
.WithApiUrl("https://acme.scalegate.io")
.WithOfflineValidation(true)
.WithGracePeriod(days: 7)
.BuildAndActivateAsync();
// Later, check if running in offline mode
if (client.Status == LicenseStatus.GracePeriod)
{
var daysLeft = client.LicenseInfo.GraceDaysRemaining;
ShowBanner($"Offline mode: {daysLeft} days remaining. Connect to revalidate.");
}
Auto-Update
The SDK includes a built-in update mechanism for distributing new versions of your desktop app.
// 1. Check for updates
var update = await client.CheckForUpdateAsync(currentVersion: "1.2.0");
if (update.IsAvailable)
{
Console.WriteLine($"Version {update.LatestVersion} available");
Console.WriteLine($"Release notes: {update.ReleaseNotes}");
// 2. Download with progress
var filePath = await client.DownloadUpdateAsync(
update,
progress: (percent, bytesReceived, totalBytes) =>
{
progressBar.Value = percent;
statusLabel.Text = $"Downloading... {percent}%";
}
);
// 3. Apply and restart
var applied = await client.ApplyUpdateAsync(filePath, restartApp: true);
}
| Property | Type | Description |
|---|---|---|
IsAvailable | bool | Whether a newer version exists |
LatestVersion | string | Semantic version string (e.g. "2.0.1") |
ReleaseNotes | string | Markdown release notes |
DownloadUrl | string | URL for download (used internally by SDK) |
FileSize | long | File size in bytes |
IsMandatory | bool | Whether the update is required |
Events
The SDK raises events when license state changes. Subscribe to react in real-time.
// Status change event
client.StatusChanged += (sender, args) =>
{
Console.WriteLine($"Status: {args.OldStatus} -> {args.NewStatus}");
if (args.NewStatus == LicenseStatus.Expired)
ShowRenewalDialog();
};
// Revalidation event (fires after each auto-revalidation check)
client.Revalidated += (sender, args) =>
{
if (!args.Success)
Console.WriteLine($"Revalidation failed: {args.Message}");
};
// Grace period warning
client.GracePeriodWarning += (sender, args) =>
{
ShowBanner($"Offline for {args.DaysElapsed} days. {args.DaysRemaining} days left.");
};
// Update available event
client.UpdateAvailable += (sender, args) =>
{
ShowUpdatePrompt(args.LatestVersion, args.ReleaseNotes);
};
| Event | Args Type | Description |
|---|---|---|
StatusChanged | LicenseStatusChangedArgs | Fires when license status transitions |
Revalidated | RevalidationArgs | Fires after each periodic revalidation |
GracePeriodWarning | GracePeriodArgs | Fires daily during offline grace period |
UpdateAvailable | UpdateAvailableArgs | Fires when auto-update check finds a new version |
ActivationFailed | ActivationFailedArgs | Fires when activation attempt fails |
Complete Integration Example
A production-ready WPF example that handles every scenario:
using System.Windows;
using ScaleGate.Licensing.SDK;
namespace MyApp;
public partial class MainWindow : Window
{
private LicenseClient? _license;
public MainWindow()
{
InitializeComponent();
Loaded += async (_, _) => await InitializeLicenseAsync();
}
private async Task InitializeLicenseAsync()
{
var key = LoadSavedKey() ?? await ShowKeyEntryDialogAsync();
if (string.IsNullOrEmpty(key)) { Close(); return; }
try
{
_license = await LicenseClient
.Create("app_xxxxxxxxxxxx", key)
.WithApiUrl("https://acme.scalegate.io")
.WithOfflineValidation(true)
.WithGracePeriod(days: 7)
.WithAutoRevalidation(intervalMinutes: 60)
.BuildAndActivateAsync();
// Wire up events
_license.StatusChanged += OnStatusChanged;
_license.GracePeriodWarning += OnGraceWarning;
_license.UpdateAvailable += OnUpdateAvailable;
HandleStatus(_license.Status);
SaveKey(key);
}
catch (LicenseActivationException ex)
{
MessageBox.Show($"Activation failed: {ex.Message}",
"License Error", MessageBoxButton.OK, MessageBoxImage.Error);
Close();
}
}
private void HandleStatus(LicenseStatus status)
{
switch (status)
{
case LicenseStatus.Active:
StatusBar.Text = $"Licensed to {_license!.LicenseInfo.CustomerName}";
MainContent.IsEnabled = true;
break;
case LicenseStatus.GracePeriod:
StatusBar.Text = "Offline mode - connect to revalidate";
MainContent.IsEnabled = true;
break;
case LicenseStatus.Expired:
StatusBar.Text = "License expired";
MainContent.IsEnabled = false;
ShowRenewalPrompt();
break;
default:
StatusBar.Text = "Unlicensed";
MainContent.IsEnabled = false;
break;
}
}
private void OnStatusChanged(object? s, LicenseStatusChangedArgs e)
=> Dispatcher.Invoke(() => HandleStatus(e.NewStatus));
private void OnGraceWarning(object? s, GracePeriodArgs e)
=> Dispatcher.Invoke(() =>
ShowBanner($"{e.DaysRemaining} days of offline access remaining"));
private void OnUpdateAvailable(object? s, UpdateAvailableArgs e)
=> Dispatcher.Invoke(() =>
ShowUpdateDialog(e.LatestVersion, e.ReleaseNotes));
}
🌐 Web SDK
The JavaScript/TypeScript SDK for browser and server-side licensing in web applications.
Installation
npm i @scalegate/licensing-web-sdk
yarn add @scalegate/licensing-web-sdk
pnpm add @scalegate/licensing-web-sdk
<script src="https://cdn.scalegate.io/sdk/web/latest/scalegate.min.js"></script>
React Quick Start
A complete React hook for license management:
import { useState, useEffect } from 'react';
import { ScaleGate } from '@scalegate/licensing-web-sdk';
export function useLicense(licenseKey: string) {
const [status, setStatus] = useState<'loading' | 'active' | 'expired' | 'invalid'>('loading');
const [features, setFeatures] = useState<string[]>([]);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const sg = new ScaleGate({
appId: 'YOUR_APP_ID',
apiUrl: 'https://acme.scalegate.io',
heartbeatInterval: 300, // seconds
});
sg.initialise(licenseKey)
.then((result) => {
setStatus(result.status);
setFeatures(result.features || []);
})
.catch((err) => {
setStatus('invalid');
setError(err.message);
});
return () => sg.destroy();
}, [licenseKey]);
return { status, features, error };
}
// Usage in a component:
function App() {
const { status, features } = useLicense('XXXX-XXXX-XXXX-XXXX');
if (status === 'loading') return <Spinner />;
if (status !== 'active') return <PayWall />;
return (
<div>
<h1>Welcome!</h1>
{features.includes('pro-charts') && <ProCharts />}
</div>
);
}
Next.js (SSR)
Server-side validation in a Next.js API route or server component:
import { ScaleGateServer } from '@scalegate/licensing-web-sdk/server';
import { NextResponse } from 'next/server';
const sg = new ScaleGateServer({
appId: process.env.SCALEGATE_APP_ID!,
apiKey: process.env.SCALEGATE_API_KEY!,
apiUrl: process.env.SCALEGATE_API_URL!,
});
export async function POST(req: Request) {
const { licenseKey } = await req.json();
const result = await sg.validate(licenseKey, {
domain: req.headers.get('origin') || undefined,
});
if (result.status !== 'active') {
return NextResponse.json(
{ error: 'Invalid license', status: result.status },
{ status: 403 }
);
}
return NextResponse.json({
valid: true,
features: result.features,
expiresAt: result.expiresAt,
});
}
Vue.js
Vue 3 composition API example:
import { ref, onMounted, onUnmounted } from 'vue';
import { ScaleGate } from '@scalegate/licensing-web-sdk';
export function useLicense(licenseKey: string) {
const status = ref('loading');
const features = ref<string[]>([]);
let sg: ScaleGate;
onMounted(async () => {
sg = new ScaleGate({
appId: 'YOUR_APP_ID',
apiUrl: 'https://acme.scalegate.io',
});
const result = await sg.initialise(licenseKey);
status.value = result.status;
features.value = result.features || [];
});
onUnmounted(() => sg?.destroy());
return { status, features };
}
Vanilla JavaScript
<script src="https://cdn.scalegate.io/sdk/web/latest/scalegate.min.js"></script>
<script>
const sg = new ScaleGate.Client({
appId: 'YOUR_APP_ID',
apiUrl: 'https://acme.scalegate.io',
});
sg.initialise('XXXX-XXXX-XXXX-XXXX')
.then(function(result) {
if (result.status === 'active') {
document.getElementById('app').style.display = 'block';
} else {
document.getElementById('paywall').style.display = 'block';
}
});
</script>
Node.js (Express Middleware)
import express from 'express';
import { ScaleGateServer } from '@scalegate/licensing-web-sdk/server';
const sg = new ScaleGateServer({
appId: process.env.SCALEGATE_APP_ID!,
apiKey: process.env.SCALEGATE_API_KEY!,
apiUrl: process.env.SCALEGATE_API_URL!,
});
// Middleware that validates license on every request
export const requireLicense = async (req, res, next) => {
const key = req.headers['x-license-key'];
if (!key) return res.status(401).json({ error: 'License key required' });
const result = await sg.validate(key, { domain: req.hostname });
if (result.status !== 'active') {
return res.status(403).json({ error: 'License invalid', status: result.status });
}
req.license = result;
next();
};
// Usage
const app = express();
app.use('/api', requireLicense);
app.get('/api/data', (req, res) => {
res.json({ data: 'protected content', features: req.license.features });
});
Configuration Reference
| Option | Type | Default | Description |
|---|---|---|---|
appId | string | Required | Your application ID from the dashboard |
apiUrl | string | Required | Your ScaleGate tenant URL |
apiKey | string | - | Server-side only. API key for S2S calls |
heartbeatInterval | number | 300 | Seconds between heartbeat pings (0 = off) |
onStatusChange | function | - | Callback when license status changes |
timeout | number | 10000 | Request timeout in milliseconds |
retries | number | 2 | Number of automatic retries on failure |
Operations
initialise (Browser)
const result = await sg.initialise('XXXX-XXXX-XXXX-XXXX');
// result: { status, features, customerName, expiresAt, sessionId }
validate
const result = await sg.validate('XXXX-XXXX-XXXX-XXXX');
// result: { valid, status, features, expiresAt, message }
checkFeatures
const features = await sg.checkFeatures('XXXX-XXXX-XXXX-XXXX');
// features: { enabled: ['pro-charts', 'export-pdf'], plan: 'professional' }
heartbeat
// Heartbeat is automatic when heartbeatInterval > 0
// Manual heartbeat:
const hb = await sg.heartbeat('XXXX-XXXX-XXXX-XXXX');
// hb: { alive: true, status: 'active' }
Feature Flags
Gate UI features based on the customer's license tier:
import { useLicense } from './useLicense';
function Dashboard() {
const { status, features } = useLicense(userLicenseKey);
const hasAdvancedAnalytics = features.includes('advanced-analytics');
const hasExportPdf = features.includes('export-pdf');
const hasWhiteLabel = features.includes('white-label');
return (
<div>
<BasicDashboard />
{hasAdvancedAnalytics ? (
<AnalyticsPanel />
) : (
<UpgradeCard feature="Advanced Analytics" />
)}
{hasExportPdf && <ExportButton format="pdf" />}
{hasWhiteLabel && <BrandingSettings />}
</div>
);
}
⚛ REST API Reference
For developers using languages without an official SDK. All endpoints accept and return JSON.
https://your-tenant.scalegate.io -Replace with your actual tenant subdomain.Common Headers
| Header | Required | Description |
|---|---|---|
Content-Type | Yes | application/json |
X-App-Id | Yes | Your application ID |
X-Api-Key | S2S only | Server-side API key (never expose in client code) |
Activate License
Activates a license key for a specific device. Returns the full license status and metadata.
Request Body
{
"licenseKey": "XXXX-XXXX-XXXX-XXXX",
"deviceFingerprint": "hw_abc123...",
"deviceName": "John's Workstation",
"appVersion": "1.2.0"
}
Success Response (200)
{
"status": "active",
"customerName": "Acme Corp",
"customerEmail": "admin@acme.com",
"expiresAt": "2027-01-15T00:00:00Z",
"features": ["pro-charts", "export-pdf"],
"maxDevices": 5,
"activeDevices": 2,
"message": "Activation successful"
}
{ "status": "invalid", "message": "License key not found" }
{ "status": "device_limit_reached", "message": "Maximum device limit (5) reached", "maxDevices": 5 }
{ "status": "expired", "message": "License expired on 2026-01-15", "expiresAt": "2026-01-15T00:00:00Z" }
{ "status": "revoked", "message": "This license has been revoked" }
Code Examples
curl -X POST https://acme.scalegate.io/api/licenses/activate \
-H "Content-Type: application/json" \
-H "X-App-Id: YOUR_APP_ID" \
-d '{
"licenseKey": "XXXX-XXXX-XXXX-XXXX",
"deviceFingerprint": "hw_abc123",
"deviceName": "My Workstation",
"appVersion": "1.2.0"
}'
import requests
resp = requests.post(
"https://acme.scalegate.io/api/licenses/activate",
headers={"X-App-Id": "YOUR_APP_ID"},
json={
"licenseKey": "XXXX-XXXX-XXXX-XXXX",
"deviceFingerprint": "hw_abc123",
"deviceName": "My Workstation",
"appVersion": "1.2.0",
},
)
data = resp.json()
print(data["status"]) # "active"
HttpClient client = HttpClient.newHttpClient();
String body = """
{"licenseKey":"XXXX-XXXX-XXXX-XXXX",
"deviceFingerprint":"hw_abc123",
"deviceName":"My Workstation",
"appVersion":"1.2.0"}""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://acme.scalegate.io/api/licenses/activate"))
.header("Content-Type", "application/json")
.header("X-App-Id", "YOUR_APP_ID")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]string{
"licenseKey": "XXXX-XXXX-XXXX-XXXX",
"deviceFingerprint": "hw_abc123",
"deviceName": "My Workstation",
"appVersion": "1.2.0",
})
req, _ := http.NewRequest("POST",
"https://acme.scalegate.io/api/licenses/activate",
bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-App-Id", "YOUR_APP_ID")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
fmt.Println(resp.StatusCode)
}
<?php
$ch = curl_init('https://acme.scalegate.io/api/licenses/activate');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-App-Id: YOUR_APP_ID',
],
CURLOPT_POSTFIELDS => json_encode([
'licenseKey' => 'XXXX-XXXX-XXXX-XXXX',
'deviceFingerprint' => 'hw_abc123',
'deviceName' => 'My Workstation',
'appVersion' => '1.2.0',
]),
]);
$response = curl_exec($ch);
$data = json_decode($response, true);
echo $data['status'];
require 'net/http'
require 'json'
uri = URI('https://acme.scalegate.io/api/licenses/activate')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
req = Net::HTTP::Post.new(uri)
req['Content-Type'] = 'application/json'
req['X-App-Id'] = 'YOUR_APP_ID'
req.body = {
licenseKey: 'XXXX-XXXX-XXXX-XXXX',
deviceFingerprint: 'hw_abc123',
deviceName: 'My Workstation',
appVersion: '1.2.0'
}.to_json
res = http.request(req)
data = JSON.parse(res.body)
puts data['status']
use reqwest;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let resp = client
.post("https://acme.scalegate.io/api/licenses/activate")
.header("X-App-Id", "YOUR_APP_ID")
.json(&json!({
"licenseKey": "XXXX-XXXX-XXXX-XXXX",
"deviceFingerprint": "hw_abc123",
"deviceName": "My Workstation",
"appVersion": "1.2.0"
}))
.send().await?;
println!("{}", resp.text().await?);
Ok(())
}
import Foundation
let url = URL(string: "https://acme.scalegate.io/api/licenses/activate")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("YOUR_APP_ID", forHTTPHeaderField: "X-App-Id")
let body: [String: Any] = [
"licenseKey": "XXXX-XXXX-XXXX-XXXX",
"deviceFingerprint": "hw_abc123",
"deviceName": "My Workstation",
"appVersion": "1.2.0"
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
val client = OkHttpClient()
val json = """
{"licenseKey":"XXXX-XXXX-XXXX-XXXX",
"deviceFingerprint":"hw_abc123",
"deviceName":"My Workstation",
"appVersion":"1.2.0"}"""
val body = json.toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url("https://acme.scalegate.io/api/licenses/activate")
.addHeader("X-App-Id", "YOUR_APP_ID")
.post(body)
.build()
val response = client.newCall(request).execute()
println(response.body?.string())
import 'package:http/http.dart' as http;
import 'dart:convert';
final response = await http.post(
Uri.parse('https://acme.scalegate.io/api/licenses/activate'),
headers: {
'Content-Type': 'application/json',
'X-App-Id': 'YOUR_APP_ID',
},
body: jsonEncode({
'licenseKey': 'XXXX-XXXX-XXXX-XXXX',
'deviceFingerprint': 'hw_abc123',
'deviceName': 'My Workstation',
'appVersion': '1.2.0',
}),
);
final data = jsonDecode(response.body);
print(data['status']);
Validate License
Validates an active license for a specific device. Returns current status and features.
Request Body
{
"licenseKey": "XXXX-XXXX-XXXX-XXXX",
"deviceFingerprint": "hw_abc123..."
}
Success Response (200)
{
"valid": true,
"status": "active",
"features": ["pro-charts", "export-pdf"],
"expiresAt": "2027-01-15T00:00:00Z",
"message": "License is valid"
}
Check for Update
Checks if a newer version of the application is available for download.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
appId | string | Your application ID |
version | string | Current app version (semver) |
key | string | License key (to verify update entitlement) |
Success Response (200)
{
"updateAvailable": true,
"latestVersion": "2.0.1",
"downloadUrl": "https://acme.scalegate.io/dl/myapp-2.0.1.exe",
"releaseNotes": "Bug fixes and performance improvements",
"fileSize": 15728640,
"isMandatory": false
}
Web Initialise
Initialises a web license session. Creates a session tied to the requesting origin domain.
Request Body
{
"licenseKey": "XXXX-XXXX-XXXX-XXXX",
"domain": "myapp.example.com"
}
Success Response (200)
{
"status": "active",
"sessionId": "sess_xxxxxxxxxxxxxxxx",
"customerName": "Acme Corp",
"features": ["pro-charts", "export-pdf"],
"expiresAt": "2027-01-15T00:00:00Z"
}
Web Validate
Request Body
{
"licenseKey": "XXXX-XXXX-XXXX-XXXX",
"domain": "myapp.example.com"
}
Success Response (200)
{
"valid": true,
"status": "active",
"features": ["pro-charts", "export-pdf"],
"expiresAt": "2027-01-15T00:00:00Z"
}
Web Features
Returns the list of feature flags enabled for a license key.
Request Body
{
"licenseKey": "XXXX-XXXX-XXXX-XXXX"
}
Success Response (200)
{
"enabled": ["pro-charts", "export-pdf", "advanced-analytics"],
"plan": "professional"
}
Web Heartbeat
Lightweight ping to confirm a web license session is still active. Returns minimal data for performance.
Success Response (200)
{
"alive": true,
"status": "active"
}
HTTP Status Codes
| Code | Meaning |
|---|---|
| 200 | Request succeeded |
| 400 | Bad request (missing fields, malformed key) |
| 401 | Missing or invalid App ID / API key |
| 403 | License expired, revoked, or device limit reached |
| 404 | License key not found |
| 429 | Rate limit exceeded (wait and retry) |
| 500 | Server error (retry with backoff) |
⚠ Error Reference
Complete reference of all license statuses and error conditions.
| Status | HTTP | Meaning | Common Cause | Resolution |
|---|---|---|---|---|
active | 200 | License is valid | - | No action needed |
invalid | 400 | Key not found or malformed | Typo in license key, wrong app ID | Verify key format and app ID |
expired | 403 | License has expired | Subscription lapsed | Renew from the dashboard or call RenewAsync() |
revoked | 403 | Key permanently revoked | Refund, abuse, or manual revocation | Contact support or issue a new key |
suspended | 403 | Temporarily suspended | Payment issue, admin action | Resolve payment or contact support |
device_limit_reached | 403 | Too many active devices | Key used on more devices than allowed | Deactivate a device in the dashboard |
domain_mismatch | 403 | Domain not authorized | Web key used on wrong domain | Add domain in the dashboard settings |
trial_expired | 403 | Trial period ended | Trial key reached its time limit | Upgrade to a paid license |
rate_limited | 429 | Too many requests | Excessive API calls in short window | Implement exponential backoff |
server_error | 500 | Internal server error | Transient infrastructure issue | Retry with exponential backoff |
★ Best Practices
Follow these guidelines to build robust, secure licensed software.
Do
- Enable offline validation for desktop apps that may lose connectivity
- Set a reasonable grace period (3-7 days) for offline use
- Handle every LicenseStatus case in your switch statements
- Subscribe to StatusChanged events for real-time updates
- Use auto-revalidation to catch revocations promptly
- Store API keys in environment variables, never in source code
- Implement graceful degradation (show paywall, not crash)
- Log license errors for troubleshooting (do not log the key itself)
Don't
- Don't hardcode license keys in source code
- Don't expose your server-side API key in client-side code
- Don't call validate on every user action (use cached status)
- Don't ignore expired/revoked status (your users will find the loophole)
- Don't use device fingerprint as an authentication mechanism
- Don't retry failed activations in a tight loop (use backoff)
- Don't bypass SSL certificate validation in production
- Don't display raw error messages to end users
Security Checklist
Protect Your API Key
Server-side API keys should only be used in backend code. Never bundle them in client-side JavaScript or desktop executables.
Enable Offline Validation
For desktop apps, always enable offline validation with a grace period. This ensures your app works even with intermittent connectivity.
Pin Your SDK Version
Use a specific version range in your package reference to avoid breaking changes from automatic major version bumps.
Use Auto-Update
Deliver security patches and improvements instantly. Mandatory updates ensure all users are on the latest secure version.
Performance Tips
client.Status (which reads from memory) instead of calling ValidateAsync() on every operation. Auto-revalidation handles periodic server checks in the background.
checkFeatures() once on app init and cache the result. Features rarely change mid-session, so polling is unnecessary.
❓ Frequently Asked Questions
expiresAt field in responses will be null for perpetual licenses. You can still revoke perpetual licenses at any time.