Skip to main content

C# SDK

Licensr.Sdk wraps the licensing API: validation, seat/domain activation, offline EdDSA token verification, and hosted checkout. Targets netstandard2.1 and net8.0; async-first with System.Text.Json source generation throughout so it stays IL2CPP-safe. Its only runtime dependency is BouncyCastle.Cryptography — pure managed code, no P/Invoke — for offline EdDSA verification, mirroring the JS/TS SDK's single dependency on jose for the same job.

Which hosts can load it​

The SDK needs a host on .NET Core 3.0+ / .NET 5+, or Unity 2021.2+. That covers cTrader, Revit and AutoCAD 2025+, and Rhino 8.

It does not load in .NET Framework 4.8 hosts: NinjaTrader 8, Revit and AutoCAD 2024 and earlier, and Rhino 7. For those, make the plain web calls yourself with HttpClient. The any-language guide shows how.

Install​

NuGet (.NET desktop/server, Godot's Mono backend, cTrader plugins):

dotnet add package Licensr.Sdk

Unity (two steps):

  1. Package Manager → Install package from git URL:
https://github.com/tekunodev/licensr-dotnet.git?path=upm
  1. Install BouncyCastle.Cryptography as well (e.g. NuGetForUnity). It is not bundled, so it does not clash with other assets that already ship it. Offline verify will not work until this assembly is in the project.

Quickstart​

using Licensr.Sdk;

var client = new LicensrClient(new LicensrClientConfig
{
ApiKey = "pk_live_...", // safe to embed in a distributed build — see the embedded-key doctrine below
PluginSlug = "my-plugin",
});

var result = await client.ValidateAsync(new ValidateRequest { LicenseKey = userEnteredKey });
if (result.Valid)
{
UnlockFullFeatures();
}
else if (result.Entitlement == Entitlement.Limited)
{
UnlockDegradedMode(); // perpetual-fallback: expired, but the plan grants a limited tier
}

The result keeps what you act on at the top level: Valid, Status (why, for your message), Entitlement and FeatureFlags. Plan and limit details are grouped under result.License (PlanId, ActivationMode, MaxSeats, MaxDomains, SeatsInUse, ExpiresAt). They are for display only; never use them to decide whether to unlock.

See embedded-key doctrine. Methods return Task<T> — await them, or in a Unity coroutine use yield return new WaitUntil(() => task.IsCompleted) (or .AsUniTask() with UniTask).

API​

Every method throws on failure — see Errors.

MethodWrapsNotes
client.ValidateAsync(new ValidateRequest {LicenseKey})POST /v1/license/validateAlways read Valid/Entitlement; never branch on HTTP status — an unknown key returns 200 {valid: false}, not 404.
client.TokenAsync(new ValidateRequest {LicenseKey})POST /v1/license/tokenSame check as ValidateAsync, plus a short-lived signed offline token. See Offline verification.
client.ActivateAsync(new ActivateRequest {...})POST /v1/license/activateActivationType is Seat (per-machine) or Domain, fixed by the plugin's configured mode.
client.DeactivateAsync(new DeactivateRequest {...})POST /v1/license/deactivateFrees a seat/domain slot.
client.ActivationsAsync(new ActivationsRequest {LicenseKey})GET /v1/license/activationsLists every current activation.
client.CheckoutAsync(new CheckoutRequest {...})POST /v1/billing/checkoutReturns CheckoutUrl — direct the user there. Requires a key with the checkout scope.

Client options​

new LicensrClientConfig
{
ApiKey = "pk_live_...",
PluginSlug = "my-plugin",
BaseUrl = "https://api.licensr.app", // default; override for self-hosted/staging
DeviceId = stableHwid, // buckets rate limits per installation instead of per key — see Hardware/device IDs below
Origin = "app://my-plugin", // rarely needed — set client type Native in the admin UI
TimeoutMs = 10_000,
Retry = new RetryOptions { MaxRetries = 2, BaseDelayMs = 300, MaxDelayMs = 5000 }, // network errors / 429 / 5xx, exponential backoff + jitter, honors Retry-After
Transport = myCustomTransport, // bring your own HTTP stack — see HTTP transport below
};

Events​

client.Validated += (_, result) => Console.WriteLine($"validated: {result.Valid}");
client.Retry += (_, args) => Console.WriteLine($"retrying {args.Method}, attempt {args.Attempt} in {args.DelayMs}ms");
client.Error += (_, args) => ReportToCrashlytics(args.Method, args.Exception);

Available events: Validated, Activated, Deactivated, TokenIssued, Retry, Error.

Offline verification​

client.TokenAsync() mints an EdDSA-signed JWT whose claims mirror ValidateAsync()'s response. Verify it fully offline (no network beyond the first JWKS fetch, cached for the process lifetime):

using Licensr.Sdk.Offline;

var token = await client.TokenAsync(new ValidateRequest { LicenseKey = licenseKey });
var claims = await OfflineTokenVerifier.VerifyAsync(token.Token, token.Details.JwksUrl);
// claims.Valid, claims.Entitlement, claims.Exp, ...

Offline tokens carry no revocation signal — they prove the token was genuinely issued and hasn't expired, not that the license is still active right now. Re-validate online before Exp.

To boot fully offline (no network at all, e.g. on first launch before any successful TokenAsync call), persist the last-known-good token yourself using the IOfflineTokenStore interface (InMemoryOfflineTokenStore is included, but doesn't survive a restart — back it with a config file, PlayerPrefs, or your platform's keychain).

Errors​

ExceptionWhen
LicensrApiExceptionNon-2xx response. Has Status, Code (matches the documented error codes), Message, and RetryAfterSeconds. Branch on Code, not Message — the message is for humans.
LicensrNetworkExceptionThe request never got a response (network failure, timeout, retries exhausted).
LicensrTokenVerificationExceptionOfflineTokenVerifier.VerifyAsync rejected a bad signature, wrong algorithm, or an expired/not-yet-valid token.

Hardware/device IDs​

This SDK doesn't compute a hardware ID itself — a general-purpose netstandard2.1 library has no portable way to do that, and Unity already provides one:

var config = new LicensrClientConfig
{
ApiKey = "pk_live_...",
PluginSlug = "my-plugin",
DeviceId = SystemInfo.deviceUniqueIdentifier, // Unity — reuse the same value for ActivateRequest.Identifier
};

For non-Unity .NET desktop apps, generate and persist your own stable identifier (a GUID written to a config file on first run is sufficient) and pass it as both DeviceId and the seat ActivateRequest.Identifier.

HTTP transport​

The default HttpClientTransport (backed by System.Net.Http.HttpClient) works on every Unity IL2CPP platform except WebGL, where HttpClient isn't supported. Implement IHttpTransport (one method, SendAsync) with a UnityWebRequest-backed transport for WebGL and pass it as LicensrClientConfig.Transport.

Source & issues​

Source lives in github.com/tekunodev/licensr-dotnet, mirrored from this monorepo's sdks/dotnet/. File issues and PRs there.