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
| Package | Targets | Use when |
|---|---|---|
Licet.Sdk | .NET 6 – .NET 10 | Anything with a service collection: ASP.NET Core, worker services, Umbraco 10+ |
Licet.Sdk.NetFx | .NET Framework 4.7.2 | Classic 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:
- Application ID — the
app_…id from the portal. - Trust root — the application's public signing keys, shipped with your
product as a pinned JWKS. Download
licet-jwks.jsonfrom your application's page in the portal and put it next to your app (that filename is the SDK default). - 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
InitAsyncacquire the token, or useMigrateAsyncwhen 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
OnInvalidcallbacks fire:license.OnInvalid(() => featureFlags.DisableLicensedFeatures()); -
Clock drift? Two minutes of skew are tolerated by default.
Next steps
- Sell a plan with Checkout — the flow that issues these licences.
- Handling webhooks — how the licence reaches your backend when a customer buys.