Skip to main content

Verify licences in your product

Selling a licence is half the story — this guide covers the other half: making your product honour it. The Licet SDK verifies licence tokens offline against a public key you ship with your app, so your customers' installs keep working without a network round-trip on every check.

:::caution Pre-1.0 SDK The SDK is currently version 0.4.x. The surface below is accurate today, but minor breaking changes are possible before 1.0. :::

Pick your package

PackageTargetsUse when
Licet.Sdk.NET 6 – .NET 10Anything with a service collection: ASP.NET Core, worker services, Umbraco 10+
Licet.Sdk.NetFx.NET Framework 4.7.2Classic ASP.NET / Global.asax apps, Umbraco 8-era, desktop apps without DI

Both expose the same ILicenseClient surface and share the Licet.Sdk namespaces — reference one or the other, never both in the same project.

The three inputs

Every install needs:

  1. Application ID — the app_… id from the portal.
  2. Trust root — the application's public signing keys, shipped with your product as a pinned JWKS. Download licet-jwks.json from your application's page in the portal and put it next to your app (that filename is the SDK default).
  3. Authority URL — defaults to https://api.licet.app/; you only set it for staging or self-hosted authorities.

:::caution Transport never decides trust The authority URL controls where the SDK talks; the pinned JWKS controls what it trusts. Tokens are only ever verified against the JWKS you shipped — never against whatever key the configured URL serves. That's deliberate: it stops an end customer pointing your app at a server they control and minting their own "valid" licences. Ship the JWKS with your product; don't fetch it at install time. :::

Key rotation still works offline-first: the SDK periodically refreshes the JWKS from the authority, but only accepts new keys that are cryptographically endorsed by a key it already trusts.

Set up — modern .NET

// Program.cs
builder.Services.AddLicensingSdk(builder.Configuration);
// appsettings.json
{
"Licensing": {
"ApplicationId": "app_01ABC…",
"PinnedJwksPath": "licet-jwks.json"
}
}

Registration is fail-fast — a missing application id or unresolvable JWKS throws at startup, not at first check. It also starts a background heartbeat (more below). Then inject ILicenseClient wherever you gate a feature:

public sealed class ReportExporter(ILicenseClient license)
{
public async Task<bool> CanExportAsync(CancellationToken ct)
{
var state = await license.GetStateAsync(ct: ct);
if (state is not (LicenseState.Valid or LicenseState.ValidInRefreshWindow))
return false;

return license.Has("pdf_export"); // boolean feature
}
}

Licensing more than one product from the same codebase? Register named products (Licensing:Products:{name} in config, or AddLicensingSdk("Vendor.Package", o => …)) and resolve them through ILicetLicenses.Get("Vendor.Package").

Set up — .NET Framework

No DI container, so there's a static entry point. Initialise once at startup:

// Global.asax
protected void Application_Start()
{
LicetLicense.Initialize(AppSettingsOptionsLoader.FromAppSettings());
}

protected void Application_End()
{
LicetLicense.Shutdown();
}
<!-- web.config -->
<appSettings>
<add key="Licet:ApplicationId" value="app_01ABC…" />
<add key="Licet:PinnedJwksPath" value="licet-jwks.json" />
</appSettings>

Then read LicetLicense.Current anywhere:

var license = LicetLicense.Current;
if (license.IsValid() && license.Has("pdf_export"))
{
long? maxRows = license.Limit("max_rows");
// …serve the licensed feature…
}

:::warning TimeSpan settings parse as days If you override an interval in appSettings, write "00:20:00", not "20" — a bare number parses as days, and a 20-day heartbeat is a long time to wait for a revocation. :::

Checking entitlements

The checks are pure in-memory reads over the verified token — call them as often as you like:

license.IsValid(); // signature + expiry, offline
license.Has("commerce"); // boolean feature from the plan
license.Limit("seats"); // numeric limit (long?, null if absent)
license.CheckDomain(host); // domain restriction (web products)
license.Claims; // the full token: plan, domains, expiry dates…

Feature and limit names are the entitlement definitions you created for the application in the portal — the plan the customer bought resolves to exactly this set. CheckDomain allows localhost and common staging hosts by default so licences don't get in the way of development.

GetStateAsync gives you the nuanced answer when you need to tell the user why something is off: NoToken, ExpiredToken, ValidInRefreshWindow, InvalidDomain, or Valid.

Seat and activation caps (Claims.MaxSeats, Claims.MaxActivations) are informational on the client — enforcement happens server-side when you call ActivateAsync(fingerprint) for desktop/machine activation flows.

Registering a customer's licence key

When a customer enters the licence key you issued them:

  • .NET Framework has a built-in registrar that validates before persisting:

    var result = await LicetLicense.Registrar.RegisterKeyAsync(userEnteredKey);
    if (!result.Success) { /* result.ErrorKind: MissingInput | ValidationFailed | PersistFailed */ }
  • Modern .NET has no registrar in the core package yet — the Umbraco integration ships one, but in a plain ASP.NET Core app you persist the key and let InitAsync acquire the token, or use MigrateAsync when exchanging a legacy key.

What happens offline, and when things go wrong

The token model separates two expiries: the token is short-lived and refreshes automatically; the licence (Claims.LicenseExpiresOnUtc) is the commercial entitlement. A background heartbeat (every 20 minutes by default, skipped in development) refreshes the token when it enters its refresh window and checks the revocation list when the token says it's due.

  • Network down? The cached token keeps verifying offline until its own expiry. Refresh failures never invalidate a working licence.

  • Revoked? The SDK only invalidates on a confirmed revocation from the authority — an unreachable server is treated as "still valid". When it does confirm, the snapshot is dropped and your OnInvalid callbacks fire:

    license.OnInvalid(() => featureFlags.DisableLicensedFeatures());
  • Clock drift? Two minutes of skew are tolerated by default.

Next steps