Developer Documentation v3.0

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.

.NET SDK
dotnet add package ScaleGate.Licensing.SDK
Web SDK
npm i @scalegate/licensing-web-sdk

How It Works

Four simple steps from signup to selling licensed software.

1

Create Your App

Sign in to the ScaleGate dashboard and create a new application. You'll get an App ID and API keys.

2

Install the SDK

Add the .NET or Web SDK to your project with a single package manager command.

3

Add 5 Lines

Initialize the client, pass a license key, and call activate. That's it-your app is now licensed.

4

Start Selling

Issue license keys from your dashboard. Customers activate, and you track everything in real time.

💡
Tip: You can test the full flow with a free Starter plan. No credit card required.

💻 .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:

bash
dotnet add package ScaleGate.Licensing.SDK
powershell
Install-Package ScaleGate.Licensing.SDK
xml
<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:

C#
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:

MethodTypeDefaultDescription
.Create(appId, licenseKey)string, stringRequiredYour application ID and the customer's license key
.WithApiUrl(url)stringRequiredYour ScaleGate tenant URL (e.g. https://acme.scalegate.io)
.WithOfflineValidation(enabled)boolfalseCache license data locally for offline use
.WithGracePeriod(days)int0Days the app can run offline before blocking
.WithAutoRevalidation(intervalMinutes)int0 (off)Periodic background re-check interval in minutes
.WithDeviceLimit(limit)int1Max concurrent devices per license key
.WithProxy(host, port)string, intnoneRoute API calls through an HTTP proxy
.WithTimeout(seconds)int30HTTP request timeout in seconds
.WithRetry(maxAttempts, delayMs)int, int3, 1000Retry policy for transient network errors
.WithLogger(logger)ILoggernonePass your own logger for SDK diagnostic output
.OnStatusChanged(handler)Action<LicenseStatusChangedArgs>noneRegister 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.

C#
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;
}
Activation Response Object
PropertyTypeDescription
StatusLicenseStatusThe resulting license status
CustomerNamestringRegistered customer name
CustomerEmailstringRegistered customer email
ExpiresAtDateTime?License expiration date (null for perpetual)
Featuresstring[]Array of enabled feature flags
MaxDevicesintMaximum allowed devices
ActiveDevicesintCurrent active device count
MessagestringHuman-readable status message

Validate

Validates that the current device's activation is still valid. Call this on app startup or periodically.

C#
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.

C#
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.

C#
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

StatusMeaningRecommended Action
ActiveLicense is valid and device is authorizedAllow full access
GracePeriodOffline but within grace windowShow warning, allow access
ExpiredLicense has passed its expiration datePrompt renewal, limit features
SuspendedTemporarily suspended by adminShow message, block access
RevokedPermanently revoked by adminBlock access, contact support
InvalidKey not found, malformed, or wrong appPrompt re-entry of key
DeviceLimitReachedMax concurrent device activations exceededDeactivate another device
TrialIn trial periodShow trial countdown, prompt upgrade
PendingActivationKey exists but not yet activated on this deviceCall 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.

C#
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.");
}
🛈
Note: Offline tokens are signed and tamper-resistant. If the local cache is corrupted, the SDK will require an online revalidation.

Auto-Update

The SDK includes a built-in update mechanism for distributing new versions of your desktop app.

C#
// 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);
}
Update Response Object
PropertyTypeDescription
IsAvailableboolWhether a newer version exists
LatestVersionstringSemantic version string (e.g. "2.0.1")
ReleaseNotesstringMarkdown release notes
DownloadUrlstringURL for download (used internally by SDK)
FileSizelongFile size in bytes
IsMandatoryboolWhether the update is required

Events

The SDK raises events when license state changes. Subscribe to react in real-time.

C#
// 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);
};
EventArgs TypeDescription
StatusChangedLicenseStatusChangedArgsFires when license status transitions
RevalidatedRevalidationArgsFires after each periodic revalidation
GracePeriodWarningGracePeriodArgsFires daily during offline grace period
UpdateAvailableUpdateAvailableArgsFires when auto-update check finds a new version
ActivationFailedActivationFailedArgsFires when activation attempt fails

Complete Integration Example

A production-ready WPF example that handles every scenario:

C# -MainWindow.xaml.cs
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

bash
npm i @scalegate/licensing-web-sdk
bash
yarn add @scalegate/licensing-web-sdk
bash
pnpm add @scalegate/licensing-web-sdk
html
<script src="https://cdn.scalegate.io/sdk/web/latest/scalegate.min.js"></script>

React Quick Start

A complete React hook for license management:

TypeScript -useLicense.ts
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:

TypeScript -app/api/validate/route.ts
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:

TypeScript -composables/useLicense.ts
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

HTML
<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)

TypeScript -middleware/license.ts
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

OptionTypeDefaultDescription
appIdstringRequiredYour application ID from the dashboard
apiUrlstringRequiredYour ScaleGate tenant URL
apiKeystring-Server-side only. API key for S2S calls
heartbeatIntervalnumber300Seconds between heartbeat pings (0 = off)
onStatusChangefunction-Callback when license status changes
timeoutnumber10000Request timeout in milliseconds
retriesnumber2Number of automatic retries on failure

Operations

initialise (Browser)

TypeScript
const result = await sg.initialise('XXXX-XXXX-XXXX-XXXX');
// result: { status, features, customerName, expiresAt, sessionId }

validate

TypeScript
const result = await sg.validate('XXXX-XXXX-XXXX-XXXX');
// result: { valid, status, features, expiresAt, message }

checkFeatures

TypeScript
const features = await sg.checkFeatures('XXXX-XXXX-XXXX-XXXX');
// features: { enabled: ['pro-charts', 'export-pdf'], plan: 'professional' }

heartbeat

TypeScript
// 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:

TypeScript -React
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.

🛈
Base URL: https://your-tenant.scalegate.io -Replace with your actual tenant subdomain.

Common Headers

HeaderRequiredDescription
Content-TypeYesapplication/json
X-App-IdYesYour application ID
X-Api-KeyS2S onlyServer-side API key (never expose in client code)

Activate License

POST /api/licenses/activate

Activates a license key for a specific device. Returns the full license status and metadata.

Request Body

JSON
{
  "licenseKey": "XXXX-XXXX-XXXX-XXXX",
  "deviceFingerprint": "hw_abc123...",
  "deviceName": "John's Workstation",
  "appVersion": "1.2.0"
}

Success Response (200)

JSON
{
  "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"
}
Error Responses
JSON -400 Bad Request
{ "status": "invalid", "message": "License key not found" }
JSON -403 Forbidden
{ "status": "device_limit_reached", "message": "Maximum device limit (5) reached", "maxDevices": 5 }
JSON -403 Forbidden
{ "status": "expired", "message": "License expired on 2026-01-15", "expiresAt": "2026-01-15T00:00:00Z" }
JSON -403 Forbidden
{ "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

POST /api/licenses/validate

Validates an active license for a specific device. Returns current status and features.

Request Body

JSON
{
  "licenseKey": "XXXX-XXXX-XXXX-XXXX",
  "deviceFingerprint": "hw_abc123..."
}

Success Response (200)

JSON
{
  "valid": true,
  "status": "active",
  "features": ["pro-charts", "export-pdf"],
  "expiresAt": "2027-01-15T00:00:00Z",
  "message": "License is valid"
}

Check for Update

GET /api/licenses/check-update?appId={appId}&version={version}&key={key}

Checks if a newer version of the application is available for download.

Query Parameters

ParameterTypeDescription
appIdstringYour application ID
versionstringCurrent app version (semver)
keystringLicense key (to verify update entitlement)

Success Response (200)

JSON
{
  "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

POST /api/web-licenses/initialise

Initialises a web license session. Creates a session tied to the requesting origin domain.

Request Body

JSON
{
  "licenseKey": "XXXX-XXXX-XXXX-XXXX",
  "domain": "myapp.example.com"
}

Success Response (200)

JSON
{
  "status": "active",
  "sessionId": "sess_xxxxxxxxxxxxxxxx",
  "customerName": "Acme Corp",
  "features": ["pro-charts", "export-pdf"],
  "expiresAt": "2027-01-15T00:00:00Z"
}

Web Validate

POST /api/web-licenses/validate

Request Body

JSON
{
  "licenseKey": "XXXX-XXXX-XXXX-XXXX",
  "domain": "myapp.example.com"
}

Success Response (200)

JSON
{
  "valid": true,
  "status": "active",
  "features": ["pro-charts", "export-pdf"],
  "expiresAt": "2027-01-15T00:00:00Z"
}

Web Features

POST /api/web-licenses/features

Returns the list of feature flags enabled for a license key.

Request Body

JSON
{
  "licenseKey": "XXXX-XXXX-XXXX-XXXX"
}

Success Response (200)

JSON
{
  "enabled": ["pro-charts", "export-pdf", "advanced-analytics"],
  "plan": "professional"
}

Web Heartbeat

GET /api/web-licenses/heartbeat/{licenseKey}

Lightweight ping to confirm a web license session is still active. Returns minimal data for performance.

Success Response (200)

JSON
{
  "alive": true,
  "status": "active"
}

HTTP Status Codes

CodeMeaning
200Request succeeded
400Bad request (missing fields, malformed key)
401Missing or invalid App ID / API key
403License expired, revoked, or device limit reached
404License key not found
429Rate limit exceeded (wait and retry)
500Server error (retry with backoff)

Error Reference

Complete reference of all license statuses and error conditions.

StatusHTTPMeaningCommon CauseResolution
active200 License is valid-No action needed
invalid400 Key not found or malformedTypo in license key, wrong app IDVerify key format and app ID
expired403 License has expiredSubscription lapsedRenew from the dashboard or call RenewAsync()
revoked403 Key permanently revokedRefund, abuse, or manual revocationContact support or issue a new key
suspended403 Temporarily suspendedPayment issue, admin actionResolve payment or contact support
device_limit_reached403 Too many active devicesKey used on more devices than allowedDeactivate a device in the dashboard
domain_mismatch403 Domain not authorizedWeb key used on wrong domainAdd domain in the dashboard settings
trial_expired403 Trial period endedTrial key reached its time limitUpgrade to a paid license
rate_limited429 Too many requestsExcessive API calls in short windowImplement exponential backoff
server_error500 Internal server errorTransient infrastructure issueRetry 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

Cache license status locally. After a successful validation, use client.Status (which reads from memory) instead of calling ValidateAsync() on every operation. Auto-revalidation handles periodic server checks in the background.
Use heartbeat wisely. For web apps, a 5-minute heartbeat interval is sufficient for most use cases. Avoid intervals shorter than 60 seconds as they provide diminishing returns.
Batch feature checks. Call checkFeatures() once on app init and cache the result. Features rarely change mid-session, so polling is unnecessary.

Frequently Asked Questions

What happens if my server goes down? +
If you have offline validation enabled, your desktop app users will continue to work within the configured grace period. Web SDK users will see a brief disruption but the SDK will automatically retry with exponential backoff. We recommend setting a grace period of at least 3 days for production desktop apps.
Can a user bypass the license check? +
The SDK uses hardware-based fingerprinting and cryptographically signed tokens to make circumvention difficult. No software protection is 100% unbreakable, but ScaleGate raises the bar significantly. For maximum protection, validate licenses server-side in addition to client-side checks.
How does device fingerprinting work? +
The SDK generates a unique identifier based on hardware characteristics of the device. This fingerprint is stable across reboots and minor system changes but will change if the user significantly modifies their hardware. The fingerprint is generated locally and only a hash is transmitted to the server.
Can I use ScaleGate with Electron apps? +
Yes. For Electron apps, use the Web SDK in the renderer process for feature gating and the Node.js server SDK in the main process for secure server-to-server validation. This dual approach gives you both real-time UI updates and tamper-resistant validation.
What is the rate limit for API calls? +
Rate limits depend on your plan. Starter plans allow 100 requests/minute, Professional allows 1000 requests/minute, and Enterprise plans have custom limits. Heartbeat endpoints have separate, more generous rate limits. If you receive a 429 status, implement exponential backoff.
Can I use the REST API without an SDK? +
Absolutely. The REST API is fully documented above with examples in 10 programming languages. You can integrate with any language that can make HTTP requests. The SDKs are convenience wrappers that add features like offline caching, auto-revalidation, and event subscriptions.
How do I handle license transfers between devices? +
Customers can deactivate a device from the dashboard, which frees up a device slot. The new device can then activate using the same license key. As the developer, you can also manage device activations programmatically through the admin API.
Do you support perpetual (non-expiring) licenses? +
Yes. When creating a license in the dashboard, leave the expiration date empty to create a perpetual license. The expiresAt field in responses will be null for perpetual licenses. You can still revoke perpetual licenses at any time.