ExisOne Client SDK

Embed our cross-platform .NET library to generate hardware IDs, activate and validate licenses, and send support tickets.

Package: ExisOne.Client · Version: 0.10.0 · TFMs: net8.0, net9.0

Install

dotnet add package ExisOne.Client --version 0.10.0
📦 Demo Project: See the full working example at github.com/exisllc/ExisOne.Client.Console

Initialize

using ExisOne.Client;

var client = new ExisOneClient(new ExisOneClientOptions {
	BaseUrl = "https://your-api-host", // must be https
	AccessToken = "exo_at_<public>_<secret>", // create in Access Tokens UI
	OfflinePublicKey = null, // optional: set for offline license validation
	HardwareIdOverride = null // optional: supply a per-user/per-tenant ID (e.g. Docker)
});

// Optional: change base URL later
client.WithBaseUrl("https://another-host");

// Library version
var sdkVersion = client.GetVersion();

BaseUrl is not hard-coded; you may also set environment variable EXISONE_BASEURL.

For offline license validation, set OfflinePublicKey to your tenant's RSA public key (PEM format). Obtain this from the Crypto Keys page.

Capabilities

Quickstart

// 1) Hardware ID (store locally)
var hwid = client.GenerateHardwareId();

// 2) Activation with version (user enters key/email)
var result = await client.ActivateAsync(activationKey, userEmail, hwid, "MyProduct", version: "1.0.0");
if (!result.Success) {
    if (result.ErrorCode == "version_outdated")
        Console.WriteLine($"Please upgrade to version {result.MinimumRequiredVersion}");
    else
        Console.WriteLine(result.ErrorMessage);
}

// 3) Validate on app start with version check (requires 'verify' permission)
var (isValid, status, exp, features, serverVer, minVer) = 
    await client.ValidateAsync(hwid, "MyProduct", activationKey, version: "1.0.0");

if (status == "version_outdated")
    Console.WriteLine($"Update required: minimum version is {minVer}");
else if (serverVer != null && serverVer != "1.0.0")
    Console.WriteLine($"Update available: v{serverVer}");

// 4) Optional: Deactivate from the same hardware
bool deactivated = await client.DeactivateAsync(activationKey, hwid, "MyProduct");

// 5) (Publisher tooling) Generate a new key for a product
var responseJson = await client.GenerateActivationKeyAsync("MyProduct", "user@example.com", planId: 1);

// 6) Submit support ticket from client
await client.SendSupportTicketAsync("MyProduct", userEmail, "Subject", "Message body");

Offline License Validation

For customers without internet access, generate offline activation codes from the License Keys page by providing their Hardware ID. These codes are RSA-signed and validated locally.

Setup

// Configure with your tenant's RSA public key for offline validation
var client = new ExisOneClient(new ExisOneClientOptions {
	BaseUrl = "https://your-api-host",
	OfflinePublicKey = @"-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
-----END PUBLIC KEY-----"
});

Smart Validation (Recommended)

Auto-detects online vs offline keys based on format. Falls back to offline validation if server is unreachable.

var hwid = client.GenerateHardwareId();

// Works with both online keys (XXXX-XXXX-XXXX-XXXX) and offline codes
var result = await client.ValidateSmartAsync(licenseKeyOrOfflineCode, hwid, "MyProduct");

if (result.IsValid) {
	Console.WriteLine($"Licensed until: {result.ExpirationDate}");
	Console.WriteLine($"Offline mode: {result.WasOffline}");
	Console.WriteLine($"Features: {string.Join(",", result.Features ?? Array.Empty<string>())}");
} else {
	Console.WriteLine($"Invalid: {result.ErrorMessage}");
}

Direct Offline Validation (No Network)

// Validate completely offline - no server calls
var offlineResult = client.ValidateOffline(offlineCode, hwid);

if (offlineResult.IsValid) {
	Console.WriteLine($"Product: {offlineResult.ProductName}");
	Console.WriteLine($"Expires: {offlineResult.ExpirationDate}");
} else if (offlineResult.IsExpired) {
	Console.WriteLine("License expired");
} else if (offlineResult.HardwareMismatch) {
	Console.WriteLine("Wrong machine - license bound to different hardware");
} else {
	Console.WriteLine($"Invalid: {offlineResult.ErrorMessage}");
}

Deactivation with Opportunistic Sync

// Tries to notify server, but succeeds locally even if offline
var result = await client.DeactivateSmartAsync(keyOrCode, hwid, "MyProduct");
Console.WriteLine($"Success: {result.Success}, Server notified: {result.ServerNotified}");

Version Enforcement New in 0.5.0

Force users to upgrade their software by enabling version enforcement on your product. When enabled, activation and validation will fail if the client version is below the minimum required version.

How It Works

  1. Set Minimum Required Version on your product (e.g., "2.0.0")
  2. Enable Enforce Version Check in the Product Management page
  3. Client apps send their version during activation/validation
  4. If client version < minimum, server returns version_outdated status
  5. Server always returns serverVersion so clients know when updates are available

Activation with Version

var result = await client.ActivateAsync(key, email, hwid, "MyProduct", version: "1.5.0");

if (!result.Success) {
    if (result.ErrorCode == "version_outdated") {
        Console.WriteLine($"Your version 1.5.0 is outdated.");
        Console.WriteLine($"Minimum required: {result.MinimumRequiredVersion}");
        Console.WriteLine($"Latest available: {result.ServerVersion}");
    } else {
        Console.WriteLine($"Activation failed: {result.ErrorMessage}");
    }
} else {
    Console.WriteLine($"Activated! Server version: {result.ServerVersion}");
}

Validation with Version Check

var (isValid, status, exp, features, serverVer, minVer) = 
    await client.ValidateAsync(hwid, "MyProduct", activationKey, version: "1.5.0");

if (status == "version_outdated") {
    // Force user to upgrade
    Console.WriteLine($"Please upgrade to version {minVer} or later");
} else if (isValid) {
    // Optionally notify about available updates
    if (serverVer != "1.5.0")
        Console.WriteLine($"Update available: v{serverVer}");
}

Smart Validation with Version

var result = await client.ValidateSmartAsync(keyOrCode, hwid, "MyProduct");

// ServerVersion and MinimumRequiredVersion are included in the result
if (result.ServerVersion != null)
    Console.WriteLine($"Server version: {result.ServerVersion}");
if (result.MinimumRequiredVersion != null)
    Console.WriteLine($"Minimum required: {result.MinimumRequiredVersion}");

Offline Workflow

  1. Customer runs your app and sees their Hardware ID (from GenerateHardwareId())
  2. Customer contacts you with their Hardware ID (email/phone/support ticket)
  3. You generate an Offline Activation Code in the License Keys page
  4. Customer enters the code into your app
  5. Your app validates locally using ValidateOffline() or ValidateSmartAsync()
Security: Offline codes are RSA-SHA256 signed and embed: product ID, hardware ID, expiration date, email, and feature flags. They cannot be forged or transferred to other machines.

Hardware ID Override (Docker / Multi-Tenant) New in 0.9.0

By default, GenerateHardwareId() derives a fingerprint from the local machine (CPU, MAC, BIOS UUID, etc.). In environments where that fingerprint is unreliable or shared across users — most commonly Docker containers, multi-tenant servers, or hosted SaaS deployments — set HardwareIdOverride on the options to a stable per-user identifier (e.g. a GUID stored in your user record). When set, GenerateHardwareId() returns it verbatim and skips all local fingerprinting.

Combined with a Corporate (multi-seat) license, this lets each user inside the container consume one seat against the same license key:

// One client instance per signed-in user
var userPseudoHwid = currentUser.LicenseHardwareId; // GUID stored on the user row
var client = new ExisOneClient(new ExisOneClientOptions {
    BaseUrl = "https://your-api-host",
    AccessToken = "exo_at_PUBLIC_SECRET",
    HardwareIdOverride = userPseudoHwid
});

// GenerateHardwareId() now returns the override (no WMI / /etc/machine-id reads)
var hwid = client.GenerateHardwareId();

// Activate consumes one seat on the corporate license for this user
var result = await client.ActivateAsync(corporateKey, currentUser.Email, hwid, "MyProduct");

// On every container start (or periodic re-check), validate the same hwid
var (isValid, status, exp, features, _, _) =
    await client.ValidateAsync(hwid, "MyProduct", corporateKey);

// When a user is removed, release their seat
await client.DeactivateAsync(corporateKey, hwid, "MyProduct");
Persist the override. Generate the per-user GUID once and store it on the user record. If you regenerate it on each request you will consume a new seat every time.

For a complete Docker integration walkthrough (including admin trial check on container startup), see the Docker AI Prompt.

Corporate Seat Usage New in 0.10.0

For corporate (multi-seat) licenses, GetSeatUsageAsync() returns the current consumption and remaining capacity so your app can show customers how much of their license is in use and warn them before they hit the cap. Only meaningful for corporate licenses — standard single-device licenses return 404.

var usage = await client.GetSeatUsageAsync(corporateKey);

if (usage.IsUnlimited) {
    Console.WriteLine($"{usage.CurrentSeats} seats in use (unlimited)");
} else {
    Console.WriteLine($"{usage.CurrentSeats} / {usage.MaxSeats} seats in use");
    if (usage.Remaining <= 5)
        Console.WriteLine($"WARNING: only {usage.Remaining} seat(s) remaining");
    if (!usage.HasCapacity)
        Console.WriteLine("License is FULL. Release a seat or upgrade before adding new users.");
}

Fields on SeatUsage:

Hardware ID Caching New in 0.9.1 Hardened in 0.9.2

Starting in 0.9.1, the SDK persists the result of GenerateHardwareId() the first time it is computed and returns the cached value on every subsequent call. This makes the hardware ID stable across transient changes to the machine — Wi-Fi toggling on/off, VPN connect/disconnect, USB drives being attached, virtual NICs from Docker / Hyper-V / WSL coming and going, antivirus blocking WMI for a few seconds, and so on. Once cached, the value is frozen for the lifetime of the user profile.

The cache is stored per-user (no admin elevation required):

PlatformLocationFormat
Windows HKEY_CURRENT_USER\Software\ExisOne\Client → REG_SZ value HardwareIdEncrypted DPAPI-encrypted blob, base64 encoded 0.9.2
Linux ~/.local/share/ExisOne/hardware-id Plain text
macOS ~/Library/Application Support/ExisOne/hardware-id Plain text
Machine-bound encryption (Windows, 0.9.2+): the cached value is encrypted with DPAPI in DataProtectionScope.LocalMachine. The encryption key is derived from machine-bound material that is not extractable to other devices, so the registry value cannot be copied to another machine to clone an activation: decryption fails on the target, the SDK falls through to the algorithm, computes a different hardware ID for the new machine, and the server rejects the activation as bound to a different device.
Upgrading from 0.9.1? The 0.9.1 release wrote a plain-text value at HKCU\Software\ExisOne\Client\HardwareId. On the first call after upgrading to 0.9.2 the SDK reads that value, encrypts it in place (writing the new HardwareIdEncrypted entry), and deletes the old plain-text value. The migration is automatic and customers see no disruption.
Upgrading from 0.9.0 or earlier? The algorithm itself has not changed since 0.9.0, so existing installations produce the same hash they always have on the first call after upgrade. That value is then encrypted and cached. No re-activation is required.

When to clear the cache

You should never need to clear the cache during normal operation. For QA / support scenarios where you want to force a fresh fingerprint (e.g. testing on a freshly imaged VM, or troubleshooting an activation issue), delete the cache entry below and the next call to GenerateHardwareId() will recompute and re-cache.

# Windows (PowerShell, as the affected user)
Remove-ItemProperty -Path 'HKCU:\Software\ExisOne\Client' -Name 'HardwareIdEncrypted'

# Linux
rm ~/.local/share/ExisOne/hardware-id

# macOS
rm ~/Library/Application\ Support/ExisOne/hardware-id
Override interaction: If HardwareIdOverride is set on the client options, the cache is bypassed entirely — both reads and writes. The override is configuration, not state, and the SDK never persists it.

Permissions

MethodRequired PermissionNotes
ValidateAsyncverifyServer enforces in /api/license/validate
GenerateActivationKeyAsyncgeneratePublisher operations
DeactivateAsyncNoneAllowed only when server-side hardware matches bound license
SendSupportTicketAsyncemailSends email via tenant/global SMTP
ActivateAsyncNoneCurrent API does not require a specific scope

API Reference

// Version
string GetVersion();

// Hardware
string GenerateHardwareId();

// Activation (Updated in 0.5.0 - now returns ActivationResult)
Task<ActivationResult> ActivateAsync(string activationKey, string email, string hardwareId, string? version = null);
Task<ActivationResult> ActivateAsync(string activationKey, string email, string hardwareId, string productName, string? version = null);

// Validation (Online) - Updated in 0.5.0 to include serverVersion and minimumRequiredVersion
Task<bool> ValidateAsync(string activationKey, string hardwareId);
Task<(bool isValid, string status, DateTime? expirationDate, string[] features, string? serverVersion, string? minimumRequiredVersion)> ValidateAsync(string hardwareId, string productName, string? activationKey = null, string? version = null);

// Validation (Offline)
OfflineValidationResult ValidateOffline(string offlineCode, string hardwareId);
Task<SmartValidationResult> ValidateSmartAsync(string keyOrCode, string hardwareId, string? productName = null);

// Deactivation
Task<bool> DeactivateAsync(string activationKey, string hardwareId, string productName);
Task<DeactivationResult> DeactivateSmartAsync(string keyOrCode, string hardwareId, string productName);

// Generation (publisher)
Task<string> GenerateActivationKeyAsync(string productName, string email, int? planId = null, int? validityDays = null);

// Support
Task SendSupportTicketAsync(string productName, string email, string subject, string message);

// Configuration
IExisOneClient WithBaseUrl(string baseUrl);

// Result Types
class ActivationResult { // New in 0.5.0
    bool Success; 
    string? ErrorCode;      // e.g., "version_outdated"
    string? ErrorMessage; 
    string? ServerVersion;  // Current product version on server
    string? MinimumRequiredVersion; // Minimum required when enforcement is on
    string? LicenseData;    // Raw license data on success
}
class OfflineValidationResult { bool IsValid; string? ErrorMessage; string? ProductName; DateTime? ExpirationDate; string[]? Features; bool IsExpired; bool HardwareMismatch; }
class SmartValidationResult { // Updated in 0.7.0
    bool IsValid; string Status; DateTime? ExpirationDate; string[]? Features; 
    bool WasOffline; string? ErrorMessage; string? ProductName;
    string? ServerVersion;  // New in 0.5.0
    string? MinimumRequiredVersion; // New in 0.5.0
}
class DeactivationResult { bool Success; bool ServerNotified; string? ErrorMessage; }

Common Errors

Demo & Examples

A complete working demo console application is available on GitHub: ExisOne.Client.Console

The demo includes interactive examples of all SDK features:

git clone https://github.com/exisllc/ExisOne.Client.Console.git
cd ExisOne.Client.Console
dotnet run --project ExisOne.Client.Console

Changelog

See Also