ExisOne Client SDK
Embed our cross-platform .NET library to generate hardware IDs, activate and validate licenses, and send support tickets.
ExisOne.Client · Version: 0.10.0 · TFMs: net8.0, net9.0
Install
dotnet add package ExisOne.Client --version 0.10.0
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
- Hardware ID:
GenerateHardwareId()— cross-platform fingerprint (salted SHA-256), or returnsHardwareIdOverridewhen set New in 0.9.0 - Activate Updated in 0.5.0:
ActivateAsync(key, email, hardwareId, productName, version?)→ returnsActivationResultwith success/error details and server version info - Validate:
ValidateAsync(key, hardwareId)→ booleanValidateAsync(hardwareId, productName, activationKey?, version?)→ returns(isValid, status, expirationDate, features[], serverVersion, minimumRequiredVersion)Updated in 0.5.0
- Deactivate:
DeactivateAsync(key, hardwareId, productName)(requires the same hardware) - Generate key:
GenerateActivationKeyAsync(productName, email, planId?, validityDays?)(requiresgenerate) - Support ticket:
SendSupportTicketAsync(productName, email, subject, message)(requiresemail) - Offline Validation:
ValidateOffline(offlineCode, hardwareId)→ validates locally without server connectionValidateSmartAsync(keyOrCode, hardwareId, productName?)→ auto-detects online/offline, falls back gracefully; includesServerVersionandMinimumRequiredVersionUpdated in 0.5.0DeactivateSmartAsync(keyOrCode, hardwareId, productName)→ opportunistic server sync
- Version Enforcement New in 0.5.0:
- Pass your app's version during activation and validation
- Server returns
serverVersionandminimumRequiredVersionso you can notify users of updates - When enforcement is enabled on the product, outdated clients receive
version_outdatedstatus
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
- Set Minimum Required Version on your product (e.g., "2.0.0")
- Enable Enforce Version Check in the Product Management page
- Client apps send their version during activation/validation
- If client version < minimum, server returns
version_outdatedstatus - Server always returns
serverVersionso 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
- Customer runs your app and sees their Hardware ID (from
GenerateHardwareId()) - Customer contacts you with their Hardware ID (email/phone/support ticket)
- You generate an Offline Activation Code in the License Keys page
- Customer enters the code into your app
- Your app validates locally using
ValidateOffline()orValidateSmartAsync()
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");
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:
CurrentSeats— number of active activationsMaxSeats— configured limit;nullor 0 means unlimitedHasCapacity—trueif at least one more activation will succeedIsUnlimited—truewhen there is no seat limitRemaining— computed seats still available,nullwhen unlimited, clamped at 0
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):
| Platform | Location | Format |
|---|---|---|
| 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 |
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.
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.
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
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
| Method | Required Permission | Notes |
|---|---|---|
ValidateAsync | verify | Server enforces in /api/license/validate |
GenerateActivationKeyAsync | generate | Publisher operations |
DeactivateAsync | None | Allowed only when server-side hardware matches bound license |
SendSupportTicketAsync | email | Sends email via tenant/global SMTP |
ActivateAsync | None | Current 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
- 403 verify permission required: Add
verifyto the token in Access Tokens UI. - Activation key not found in this tenant: Token and key must belong to same tenant.
- 400 Bad Request on activation: Ensure product name is correct (or use overload without productName).
- version_outdated New: Client version is below the minimum required. Check
MinimumRequiredVersionin the response and prompt user to upgrade.
Demo & Examples
A complete working demo console application is available on GitHub: ExisOne.Client.Console
The demo includes interactive examples of all SDK features:
- Hardware ID generation
- License activation with version check
- Simple and rich validation
- Smart validation (online/offline auto-detect)
- Offline validation
- Deactivation (standard and smart)
- Support ticket submission
- Key generation (publisher feature)
git clone https://github.com/exisllc/ExisOne.Client.Console.git cd ExisOne.Client.Console dotnet run --project ExisOne.Client.Console
Changelog
- 0.10.0: New Corporate Seat Usage API —
New
GetSeatUsageAsync(activationKey)method returns current seat consumption, max seats, remaining capacity, and unlimited flag for corporate licenses. Wraps theGET /api/license/{activationKey}/usageendpoint so you no longer need to make raw HTTP calls to surface seat counts in your app. Additive change — no existing signatures modified. - 0.9.2: Security Machine-Bound Cache Encryption —
The Windows hardware ID cache is now encrypted with DPAPI in
DataProtectionScope.LocalMachineand stored atHKCU\Software\ExisOne\Client\HardwareIdEncrypted. Copying the registry value to another machine no longer clones an activation: decryption fails on the target machine, the SDK falls through to the algorithm, and the new machine reports a different hardware ID that the server rejects. Existing 0.9.1 plain-text caches are migrated in place on first call. The algorithm and the override path are unchanged. Linux/macOS file caches are unchanged. - 0.9.1: New Hardware ID Caching —
GenerateHardwareId()now persists its result toHKCU\Software\ExisOne\Clienton Windows (or~/.local/share/ExisOne/hardware-idon Linux / macOS) and returns the cached value on every subsequent call. Stops the hardware ID from drifting due to transient machine state — Wi-Fi toggling, VPN connect/disconnect, USB drives, virtual NICs, WMI hiccups, etc. The algorithm itself is unchanged, so existing customers see no disruption on upgrade. Cache is bypassed entirely whenHardwareIdOverrideis set. - 0.9.0: New Hardware ID Override —
New
HardwareIdOverrideoption onExisOneClientOptions; when set,GenerateHardwareId()returns it verbatim and skips local fingerprinting. Designed for Docker containers and multi-tenant servers where each user should consume one seat on a corporate license. See the new Docker AI Prompt. - 0.7.0: New Feature Names —
Features now return names (
string[]) instead of codes (int[]); validation always returns product features even for invalid/unlicensed responses (if product exists); breaking change: update code that expectsint[]features to usestring[]. - 0.6.0: Consistent Expiration Dates —
Validation now always returns
expirationDateeven for invalid/inactive licenses; for invalid cases, expiration is calculated from device record creation date + trial days (if product found) or just creation date; device license records are now cleared when a license is deactivated; new device history API endpoint for viewing all license events by hardware ID. - 0.5.0: Version Enforcement —
ActivateAsync()now accepts optionalversionparameter and returnsActivationResultwith structured error handling;ValidateAsync()tuple now returnsserverVersionandminimumRequiredVersion;SmartValidationResultincludes version info; newversion_outdatedstatus when client version is below minimum required; server always returns current product version for update notifications. - 0.4.0: Offline License Validation — Added
ValidateOffline(),ValidateSmartAsync(),DeactivateSmartAsync(); newOfflinePublicKeyoption for RSA-signed offline codes; smart auto-detection of online vs offline keys; opportunistic server sync for deactivation. - 0.3.0: Added
DeactivateAsync(...); license history now recordsIpAddressandCountryon activation/validation/deactivation; docs updated. - 0.2.0: Added rich
ValidateAsync(hardwareId, productName, ...)overload returning status/expiration/features; docs updated. - 0.1.1: Multi-target
net8.0/net9.0,ValidateAsyncuses secure/api/license/validate, addedGetVersion(). - 0.1.0: Initial release.