C# SDK
Official C# SDK for LicenseSeat. Add license validation to your .NET apps, Unity games, and Godot projects in minutes.
Current stable release: v0.5.0, published as LicenseSeat 0.5.0 on NuGet and
com.licenseseat.sdk0.5.0 on OpenUPM.
Building a Unity game? Use the dedicated managed Unity package. It bundles pinned dependencies, includes C# 9/linker compatibility checks, and provides a bounded
UnityWebRequesttransport. Qualify every Unity Editor, player, platform, and IL2CPP configuration you actually ship.
Important: Use only a restricted publishable client key scoped to
licenses:validate. Any credential embedded in a desktop, mobile, game, or WebGL build can be extracted; never ship an administrator or server-write key. Keep end-user license keys out of URLs, logs, crash reports, analytics, plaintext persistence, and API responses.
Installation
NuGet (.NET, Godot)
dotnet add package LicenseSeat --version 0.5.0
Requirements: a runtime implementing .NET Standard 2.0. CI exercises the package on currently supported .NET SDK/runtime lines; validate older .NET Framework hosts in your own target environment before shipping.
Unity
Option 1: Git URL (Recommended)
- Open Window > Package Manager
- Click + > Add package from git URL...
- Paste:
https://github.com/licenseseat/licenseseat-csharp.git?path=src/LicenseSeat.Unity#v0.5.0
Option 2: manifest.json
Add to Packages/manifest.json:
{
"dependencies": {
"com.licenseseat.sdk": "https://github.com/licenseseat/licenseseat-csharp.git?path=src/LicenseSeat.Unity#v0.5.0"
}
}
Option 3: OpenUPM
openupm add com.licenseseat.sdk
Unity requires 2021.3 or newer with the .NET Standard 2.1 API compatibility profile. Keep the Git URL pinned to the reviewed release tag shown above.
Quick Start
using LicenseSeat;
var client = new LicenseSeatClient(new LicenseSeatClientOptions
{
ApiKey = "pk_live_xxxxxxxx",
ProductSlug = "your-product" // Required
});
// Activate a license
var license = await client.ActivateAsync("XXXX-XXXX-XXXX-XXXX");
// Check entitlements
if (client.HasEntitlement("pro-features"))
{
// Enable pro features
}
Static API (Singleton)
For desktop apps where you want global access:
using LicenseSeat;
// Configure once at startup
LicenseSeat.LicenseSeat.Configure("pk_live_xxxxxxxx", "your-product", options =>
{
options.AutoValidateInterval = TimeSpan.FromHours(1);
});
// Use anywhere in your app
await LicenseSeat.LicenseSeat.Activate("LICENSE-KEY");
if (LicenseSeat.LicenseSeat.HasEntitlement("premium"))
{
// Premium features
}
var status = LicenseSeat.LicenseSeat.GetStatus();
var license = LicenseSeat.LicenseSeat.GetCurrentLicense();
// Cleanup on exit
LicenseSeat.LicenseSeat.Shutdown();
Configuration
Basic Configuration
var client = new LicenseSeatClient(new LicenseSeatClientOptions
{
ApiKey = "pk_live_xxxxxxxx",
ProductSlug = "your-product"
});
Advanced Configuration
var client = new LicenseSeatClient(new LicenseSeatClientOptions
{
ApiKey = "pk_live_xxxxxxxx",
ProductSlug = "your-product",
ApiBaseUrl = "https://licenseseat.com/api/v1",
AutoValidateInterval = TimeSpan.FromHours(1),
HeartbeatInterval = TimeSpan.FromMinutes(5),
AppVersion = "2.1.0",
AppBuild = "42",
MaxRetries = 3,
RetryDelay = TimeSpan.FromSeconds(1),
OfflineFallbackMode = OfflineFallbackMode.NetworkOnly,
MaxOfflineDays = 7,
MaxClockSkew = TimeSpan.FromMinutes(5),
HttpTimeout = TimeSpan.FromSeconds(30),
Debug = true
});
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
ApiKey |
string |
— | Required. Your publishable API key |
ProductSlug |
string |
— | Required. Your product identifier |
ApiBaseUrl |
string |
https://licenseseat.com/api/v1 |
API endpoint |
AutoValidateInterval |
TimeSpan |
1 hour | Background validation interval (0 = disabled) |
HeartbeatInterval |
TimeSpan |
5 minutes | Standalone heartbeat interval (0 = disabled) |
TelemetryEnabled |
bool |
true |
Attach optional device/application telemetry to supported requests |
AppVersion |
string? |
null |
Your app version for telemetry |
AppBuild |
string? |
null |
Your app build number for telemetry |
MaxRetries |
int |
3 | Retry attempts for failed requests |
RetryDelay |
TimeSpan |
1 second | Base delay between retries |
OfflineFallbackMode |
OfflineFallbackMode |
Disabled |
Offline validation mode |
MaxOfflineDays |
int |
0 | Offline grace period (0 = disabled) |
MaxClockSkew |
TimeSpan |
5 minutes | Clock tamper tolerance |
HttpTimeout |
TimeSpan |
30 seconds | Request timeout |
Debug |
bool |
false |
Enable debug logging |
Offline Fallback Modes
| Mode | Description |
|---|---|
Disabled |
Offline fallback disabled. Network failures throw exceptions. |
NetworkOnly |
Fall back only after a genuine connection, DNS, timeout, or other transport failure. Every received HTTP response—including 408, 4xx, and 5xx—remains authoritative. Recommended. |
Always |
Compatibility alias for NetworkOnly; it does not override an authoritative HTTP response or a local protocol/security failure. |
License Lifecycle
Activation
var license = await client.ActivateAsync("LICENSE-KEY");
Console.WriteLine("License activated");
Console.WriteLine($"Status: {license.Status}");
Console.WriteLine($"Plan: {license.PlanKey}");
Validation
var result = await client.ValidateAsync("LICENSE-KEY");
if (result.Valid)
{
Console.WriteLine("License is valid!");
Console.WriteLine($"Active Seats: {result.License?.ActiveSeats}/{result.License?.SeatLimit}");
}
else
{
Console.WriteLine($"Invalid: {result.Code} - {result.Message}");
}
Deactivation
await client.DeactivateAsync();
Get Status
var status = client.GetStatus();
Console.WriteLine($"Status: {status.StatusType}");
Entitlements
Simple Check
if (client.HasEntitlement("premium"))
{
// Unlock premium features
}
Detailed Check
var entitlement = client.CheckEntitlement("pro-features");
if (entitlement.Active)
{
EnableProFeatures();
}
else
{
switch (entitlement.Reason)
{
case EntitlementInactiveReason.Expired:
ShowRenewalPrompt();
break;
case EntitlementInactiveReason.NotFound:
ShowUpgradePrompt();
break;
case EntitlementInactiveReason.NoLicense:
ShowActivationPrompt();
break;
}
}
Event Handling
// Subscribe to license events
client.Events.On(LicenseSeatEvents.LicenseValidated, _ =>
Console.WriteLine("License validated!"));
client.Events.On(LicenseSeatEvents.ValidationFailed, _ =>
Console.WriteLine("Validation failed!"));
client.Events.On(LicenseSeatEvents.EntitlementChanged, _ =>
Console.WriteLine("Entitlements updated!"));
client.Events.On(LicenseSeatEvents.LicenseActivated, _ =>
Console.WriteLine("License activated"));
client.Events.On(LicenseSeatEvents.LicenseDeactivated, _ =>
Console.WriteLine("License deactivated"));
client.Events.On(LicenseSeatEvents.HeartbeatSuccess, _ =>
Console.WriteLine("Heartbeat sent"));
client.Events.On(LicenseSeatEvents.HeartbeatError, _ =>
Console.WriteLine("Heartbeat failed"));
Offline Validation
var client = new LicenseSeatClient(new LicenseSeatClientOptions
{
ApiKey = "pk_live_xxxxxxxx",
ProductSlug = "your-product",
OfflineFallbackMode = OfflineFallbackMode.NetworkOnly,
MaxOfflineDays = 7 // Allow 7 days offline
});
// Validate - falls back to the SDK's cached offline artifact if network fails
var result = await client.ValidateAsync("LICENSE-KEY");
if (result.Offline)
{
Console.WriteLine("Validated offline with cached license");
}
Offline model note: the current C# SDK still uses signed offline tokens today. Machine files are the newer preferred offline artifact at the API level, but this SDK has not migrated yet. Its built-in cache is process memory only: offline state does not survive a restart, and this release does not claim durable filesystem storage or filesystem tamper resistance.
The SDK automatically fetches and caches Ed25519-signed offline tokens after an online activation in the same process. When offline:
- Validates token signature cryptographically
- Checks token expiration (
exptimestamp) - Detects clock tampering
- Returns cached entitlements
ASP.NET Core Integration
Dependency Injection
// Program.cs
builder.Services.AddLicenseSeatClient("pk_live_xxxxxxxx", "your-product");
// Or with full options:
builder.Services.AddLicenseSeatClient(options =>
{
options.ApiKey = "pk_live_xxxxxxxx";
options.ProductSlug = "your-product";
options.AutoValidateInterval = TimeSpan.FromMinutes(30);
});
Using in Controllers
public class LicenseController : ControllerBase
{
private readonly ILicenseSeatClient _client;
public LicenseController(ILicenseSeatClient client) => _client = client;
[HttpPost("activate")]
public async Task<IActionResult> Activate([FromBody] string licenseKey)
{
var license = await _client.ActivateAsync(licenseKey);
return Ok(new { license.Status });
}
[HttpGet("status")]
public IActionResult GetStatus()
{
var status = _client.GetStatus();
return Ok(new { status.StatusType, status.Message });
}
}
Unity Integration
using UnityEngine;
using LicenseSeat;
public class LicenseController : MonoBehaviour
{
private LicenseSeatManager _manager;
void Start()
{
_manager = FindObjectOfType<LicenseSeatManager>();
// Subscribe to events
_manager.Client.Events.On(LicenseSeatEvents.LicenseValidated, _ =>
Debug.Log("License validated!"));
}
public void ActivateLicense(string licenseKey)
{
StartCoroutine(_manager.ActivateCoroutine(licenseKey, (license, error) =>
{
if (error != null)
{
Debug.LogError("License activation failed.");
return;
}
Debug.Log("License activated.");
}));
}
}
Unity SDK Features:
- Managed implementation - No native plugin; pinned managed dependencies are bundled
- IL2CPP metadata - A package
link.xmlpreserves reflection-sensitive types - WebGL transport - Uses a bounded, same-origin
UnityWebRequestadapter - Editor Tools - Settings window, inspectors
- Samples - Import from Package Manager
These compatibility checks are not proof of a real Unity or IL2CPP build. Test every Unity LTS version, player platform, and scripting backend you support.
Godot Integration
using Godot;
using LicenseSeat;
public partial class LicenseManager : Node
{
private LicenseSeatClient _client;
public override void _Ready()
{
_client = new LicenseSeatClient(new LicenseSeatClientOptions
{
ApiKey = "pk_live_xxxxxxxx",
ProductSlug = "your-product"
});
}
public async void ValidateLicense(string licenseKey)
{
var result = await _client.ValidateAsync(licenseKey);
if (result.Valid)
GD.Print("License is valid!");
else
GD.Print($"Invalid: {result.Code}");
}
public override void _ExitTree() => _client?.Dispose();
}
Error Handling
try
{
var license = await client.ActivateAsync("INVALID-KEY");
}
catch (ApiException ex) when (ex.Code == "license_not_found")
{
Console.WriteLine("License key not found");
}
catch (ApiException ex) when (ex.Code == "seat_limit_exceeded")
{
Console.WriteLine($"All {ex.Details?["seat_limit"]} seats are in use");
}
catch (ApiException ex)
{
Console.WriteLine($"API Error: {ex.Code ?? "unknown"}");
Console.WriteLine($"Status: {ex.StatusCode}");
Console.WriteLine($"Retryable: {ex.IsRetryable}");
}
Common Error Codes
license_not_found- Invalid license keylicense_expired- License has expiredlicense_suspended- License is suspendedseat_limit_exceeded- All seats are in usedevice_not_activated- Device not activated for this licenseinvalid_api_key- Invalid API key
Test Authentication
Test the configured credential against the protected, side-effect-free /auth
endpoint. The response must authenticate with the licenses:validate scope:
try
{
var authenticated = await client.TestAuthAsync();
Console.WriteLine($"Authenticated: {authenticated}");
}
catch (ApiException ex)
{
Console.WriteLine($"Auth test failed: {ex.Code ?? "unknown"} ({ex.StatusCode})");
}
Synchronous version (for Unity or contexts without async):
var authenticated = client.TestAuth();
Both forms return bool; an invalid credential, wrong scope, malformed
response, or transport failure throws instead of returning an ambiguous
partially authenticated object.
API Reference
Client Methods
| Method | Description |
|---|---|
ActivateAsync(licenseKey) |
Activate a license on this device |
ValidateAsync(licenseKey) |
Validate a license (check if valid) |
DeactivateAsync() |
Deactivate the current license |
HasEntitlement(key) |
Check if an entitlement is active |
CheckEntitlement(key) |
Get detailed entitlement status |
GetStatus() |
Get current license status |
GetCurrentLicense() |
Get the cached license |
TestAuthAsync() |
Test API key authentication and the required licenses:validate scope |
ValidationResult Properties
| Property | Type | Description |
|---|---|---|
Valid |
bool |
Whether the license is valid |
Code |
string? |
Error code if invalid |
Message |
string? |
Error message if invalid |
Offline |
bool |
True if validated offline |
License |
License? |
License data |
ActiveEntitlements |
List<Entitlement>? |
Active entitlements |
License Properties
| Property | Type | Description |
|---|---|---|
Key |
string |
The license key |
Status |
string? |
License status (active, expired, etc.) |
ExpiresAt |
DateTimeOffset? |
When the license expires |
PlanKey |
string? |
Associated plan |
SeatLimit |
int? |
Maximum allowed seats |
ActiveSeats |
int |
Currently used seats |
ActiveEntitlements |
List<Entitlement>? |
Active entitlements |
Telemetry
When TelemetryEnabled is true (the default), the SDK includes the following
optional telemetry on supported licensing requests:
| Field | Source |
|---|---|
sdk_name |
Always csharp |
sdk_version |
LicenseSeatClient.SdkVersion |
os_name |
RuntimeInformation.IsOSPlatform (Windows, macOS, Linux) |
os_version |
Environment.OSVersion.Version |
platform |
native (or unity if Unity runtime detected) |
device_model |
Environment.MachineName |
device_type |
desktop, server, or Unity device type |
architecture |
RuntimeInformation.ProcessArchitecture |
cpu_cores |
Environment.ProcessorCount |
memory_gb |
GC.GetGCMemoryInfo().TotalAvailableMemoryBytes (rounded) |
locale |
CultureInfo.CurrentCulture.Name |
language |
CultureInfo.CurrentUICulture.TwoLetterISOLanguageName |
timezone |
IANA timezone (auto-converted from Windows timezone IDs) |
runtime_version |
RuntimeInformation.FrameworkDescription (e.g., .NET 9.0.0) |
app_version |
From config, or Assembly.GetEntryAssembly version |
app_build |
From config, or AssemblyInformationalVersion |
The SDK automatically converts Windows timezone IDs (e.g., Eastern Standard Time) to IANA format (e.g., America/New_York) for consistency across platforms.
Disable the optional telemetry object with
LicenseSeatClientOptions.TelemetryEnabled = false. Core licensing still sends
the license/product context and installation fingerprint, and the server records
successful request activity and the source IP. device_model currently uses
Environment.MachineName, so treat telemetry and required licensing identifiers
as potentially linked device data rather than anonymous data.
See Telemetry for the full field reference.
Platform Support
| Platform | Package | Install |
|---|---|---|
| .NET (Console, ASP.NET, WPF, MAUI) | NuGet | dotnet add package LicenseSeat --version 0.5.0 |
| Godot 4 | NuGet | dotnet add package LicenseSeat --version 0.5.0 |
| Unity | UPM | See Unity section |
Next Steps
- C++ SDK - For native applications
- JavaScript SDK - For web applications
- Offline Licensing - Air-gapped validation
- API Reference - Direct API access