SDK integration
.NET SDK integration
A complete, zero-to-licensed walkthrough: create a product in Keyright, embed your public key with Keyright.NET, gate features offline, activate online, and issue keys to customers — failing closed by design.
This is the full journey — from an empty Keyright workspace to a shipping .NET app that unlocks paid features against a real license key. It covers both sides: the vendor setup you do once in the Keyright dashboard, and the client code you embed with the Keyright.NET SDK. Every code block uses the real SDK surface; copy them as-is.
If you only want the condensed vendor path, see Getting started. This page is the developer’s end-to-end version.
What you’ll build
You issue license keys server-side in Keyright — each tenant (your workspace) signs them with its own RSA private key that never leaves the server. Your app embeds only the matching public key and verifies licenses offline against it, so a license check needs no network. For seat enforcement and revocation you also activate online: the app exchanges a license key for a short-lived signed lease bound to the machine, then keeps working offline until that lease expires. Anything that doesn’t check out — bad signature, wrong product, expiry, revocation, a rolled-back clock — resolves to the Free edition rather than throwing. It fails closed.
Step 1 — Create your product in Keyright
Sign in to the dashboard at /dashboard (e.g. https://keyright.delta1labs.com/dashboard) and open Products → New product. Give it a name and a slug — the slug is the identifier your app’s SDK sends as Product, so pick something stable and lowercase like acme-app. Note it down; you’ll paste it into the SDK in Step 5. (The dashboard opens in its Basic view, which shows exactly the pages this walkthrough uses; the Dashboard quick reference maps every task to where it lives.)
Your workspace already has an RSA signing key, generated when the tenant was created, and New product generates a per-product key by default. Every license and lease for a product is signed with the private half, which is stored encrypted on the server and never leaves it. You only ever handle the public half.
The same thing over the API:
curl -X POST $BASE/admin/products -H "X-Admin-Token: $TOKEN" -H "content-type: application/json" \
-d '{"name":"Acme App","slug":"acme-app"}'
($BASE is your issuing service URL, $TOKEN a tenant admin/session token — see Getting started.)
Step 2 — Get your public key
Open the product’s Signing key tab (or Developers → Keys, which lists the workspace key and every product key) and copy the public key — a base64 SubjectPublicKeyInfo string. The Developers → Quickstart page goes one better and prefills the whole Step 5 snippet for you. Or fetch the workspace key from the API:
curl $BASE/admin/public-key -H "X-Admin-Token: $TOKEN"
This key is not a secret. It ships inside your compiled binary. All security comes from the private key staying on the server: the public key can only verify signatures, never mint them. Embedding it, decompiling your app to read it, even publishing it — none of that lets anyone forge a license.
Step 3 — Define your plans & entitlements
A product has tiers — the dashboard calls them editions (e.g. pro, enterprise). Each edition carries a seat count and an entitlement template — the named flags and numeric limits that get baked into every license of that edition. Entitlements are how your app asks “is this feature allowed?” at runtime. (An edition can also carry a release-coverage policy — which versions its licenses may run — covered in Step 11.)
Add one from the product’s Editions tab in the dashboard (Add edition), or over the API:
curl -X POST $BASE/admin/products/acme-app/tiers -H "X-Admin-Token: $TOKEN" -H "content-type: application/json" \
-d '{"name":"pro","seats":3,"entitlements":{"export":"true","max-projects":"10"}}'
Entitlement values are strings on the wire: "true"/"false" for flags, an integer or the literal "unlimited" for limits. A tier string maps to an edition your app can switch on — enterprise → Enterprise, anything else → Licensed, and no license at all → Free.
Step 4 — Install the SDK
Keyright.NET is on NuGet. It multi-targets netstandard2.0 and net8.0, so it runs on .NET Framework 4.8 and .NET 6–10.
dotnet add package Keyright.NET
Sibling SDKs verify the exact same license and lease formats for other runtimes — Node.js, Python, and Java — so a mixed-language product line can share one Keyright tenant and one public key. No SDK for your language? Call the HTTP API directly. This page covers the .NET client.
Step 5 — Initialize the client
Construct one KeyrightClient at startup with the product slug from Step 1, the public key from Step 2, and (for online activation) your service URL. Use the static Initialize factory — it validates the options and loads the key up front:
using Keyright.Client;
static readonly KeyrightClient License = KeyrightClient.Initialize(new KeyrightOptions
{
Product = "acme-app", // must match the product slug you issue keys for
PublicKeyBase64 = "MIIBIjANBgkq...", // the base64 public key from Step 2
ServiceUrl = "https://licensing.acme.example", // YOUR Keyright issuing service — see note below; omit if offline-only
// Optional: keep an old key valid during a rotation window (Step 9)
// AdditionalPublicKeysBase64 = { "<previous public key>" },
});
ServiceUrl is your Keyright issuing service, not ours — set it to your managed tenant’s API base URL or, if you run self-hosted Keyright, your own instance’s base URL (e.g. https://licensing.acme.example). The same SDK works against either; nothing is hardcoded to a Delta1 endpoint. Only Product and PublicKeyBase64 are required. The SDK looks for a license across several sources, in precedence order (highest first): an explicit LicenseString, then an explicit LicenseFilePath, then the KEYRIGHT_LICENSE environment variable (a path to a license file), then ConfigLicensePath, then the OS app-data path ({LocalApplicationData}/Keyright/{product}/license.json), and finally the cached activation lease. You usually set none of these — activation (Step 7) writes the lease cache for you.
Step 6 — Gate features offline
Call Validate(). It resolves the best license from the sources above, verifies the RSA signature, product match, node-lock, expiry, the optional shipped revocation list, and trial/clock-tamper state — all offline, and it never throws. On any failure it returns a LicenseInfo in the Free edition carrying the reason.
LicenseInfo info = License.Validate();
if (info.IsPaid) // true for any edition above Free
{
// unlock paid features
}
if (info.Edition == Edition.Enterprise)
{
// unlock enterprise-only features
}
Gate individual features on entitlements rather than on the edition, so changing a tier’s template doesn’t mean shipping new code:
// Boolean flag
if (License.IsEnabled("export"))
{
ShowExportCommand();
}
// Numeric limit — pass the fail-closed fallback yourself
long maxProjects = License.GetLimit("max-projects", fallback: 1);
if (currentProjectCount >= maxProjects)
{
PromptToUpgrade();
}
IsEnabled and GetLimit each validate on the spot and fail closed: a missing flag reads as disabled, a missing or unparseable limit returns your fallback. If you check several entitlements at once, validate once and reuse the result to avoid repeated work:
LicenseInfo info = License.Validate();
bool canExport = info.Entitlements.IsEnabled("export");
long maxSeats = info.Entitlements.GetLimit("max-seats", 1);
To find out why a check failed (for a “Register” dialog or diagnostics), read info.Status and the human-facing info.Message, and Validate(out LicenseSourceKind source) tells you which source won.
Step 7 — Activate online with a license key
When a customer enters a key, call ActivateAsync. It posts the key plus a stable machine id to your service, which consumes a seat and returns a short-lived signed lease bound to that machine. The SDK verifies the lease against your embedded public key and caches it locally, so every later Validate() succeeds with no network until the lease’s grace window elapses.
ActivateAsync does not throw for the ordinary failure paths (bad key, seat limit, offline, revoked) — it returns a fail-closed LicenseInfo just like Validate(). Inspect the result:
LicenseInfo info = await License.ActivateAsync(customerEnteredKey, ct);
if (info.IsValid && info.IsPaid)
{
// Activated. The lease is cached; the app now works offline until it expires.
ShowLicensedUi(info.StatusBadge); // e.g. "Enterprise" or "Enterprise Trial"
}
else
{
// Surface info.Message; the app stays in Free mode.
ShowActivationError(info.Message); // "All seats for this license are in use.", etc.
}
- Seats are enforced server-side. Activating more machines than the license allows returns a seat-limit result and no lease. Re-activating a machine that’s already bound is idempotent — no extra seat is consumed.
- Offline grace. If the service is unreachable,
ActivateAsyncfalls back to any still-valid cached lease, so a brief outage doesn’t lock the user out. When the lease nears expiry the app must reach the server again to refresh it. - Revocation takes effect on the next refresh — see Step 9.
ActivateAsync throws only for programmer errors: a missing ServiceUrl or an empty key.
Air-gapped machines. For a machine that can never reach the service, an operator signs an offline lease for its machine id (the license’s License file → Offline activation, or POST /admin/licenses/{id}/offline-lease) and delivers the JSON by file. Import it — this throws if the lease is invalid, so handle it:
try
{
LicenseInfo info = License.ImportOfflineLease(File.ReadAllText("acme.lease.json"));
}
catch (InvalidOperationException ex)
{
// wrong signature / product / machine, or expired
}
Step 8 — Issue license keys to your customers
You can hand out keys three ways:
- From the dashboard. Licenses → Issue license: pick the product and edition, set the licensee, contact email, seats and term, and Keyright generates the key and shows it once. The drawer also has a Trial toggle to mint the key as a time-limited trial (see Step 9 and Self-service free trials), and an Advanced section for feature overrides, notes and a custom key.
- Automatically from your billing provider. Wire your store’s fulfillment to
POST /webhooks/fulfill/{slug}(HMAC-signed). A “paid” event mints a license and emails the key; a “refund”/“cancel” revokes it — no manual step. - From the admin API.
POST /admin/licenses(auto-generates a key unless you pass one). See the HTTP API reference for the full request shape.
Whichever way you issue, the key is what the customer pastes into your app for ActivateAsync in Step 7.
Step 9 — Trials, node-locking, revocation & key rotation
- Trials. Turn on self-service trials per product and let customers claim a key from your own site; a trial key activates through the exact same
ActivateAsyncpath as a paid one. Show the state straight offLicenseInfo—info.IsTrial,info.StatusBadge(e.g. “Enterprise Trial”),info.ExpiryUtc, andinfo.DaysRemainingfor a “27 days left” badge. A trial’s duration is counted from first activation, and it supersedes seamlessly when the customer later activates a paid key. Full flow: Self-service free trials. - Node-locking. The SDK derives a stable machine fingerprint, with a small
NodeLockTolerance(default1) so a swapped NIC or disk doesn’t lock the user out. To control what identifies a machine, implementIMachineComponentsand setMachineComponentson the options. The activation request sends this machine id so seats are counted per device. - Revocation. Revoke a key with
POST /admin/licenses/{id}/revoke(or the dashboard’s per-row action). The client drops toFree(statusRevoked) on the next lease refresh. You can also ship a signed revocation list with your build (RevocationListJson/RevocationListPath) so a purely offline app still honours revocations. - Key rotation. When you rotate the tenant signing key, ship a build with the new public key in
PublicKeyBase64and the outgoing one inAdditionalPublicKeysBase64— licenses and leases signed by either keep validating through the transition. Drop the old key from the list once every lease signed by it has expired.
Step 10 — Test end to end
- In the dashboard, enable trials for
acme-app(a short length is fine), then issue yourself a trial key. - Run your app. Before activation,
Validate()returnsFree— your paid features stay locked. - Call
ActivateAsyncwith the trial key. Watch the app flip to the licensed state and the badge switch to e.g. “Pro Trial”. The lease is now cached on disk. - Test offline. Disconnect the network and restart the app.
Validate()still returns the licensedLicenseInfofrom the cached lease — no server round-trip — until the lease’s grace window ends. - Test revocation. Revoke the license, reconnect, and let the lease refresh; the app should drop back to
Free.
Step 11 — Release coverage (optional)
If you sell updates — “one year of updates”, “every 2.x release”, a subscription — Keyright can record which releases each license covers, sign it, and let your build decide locally whether this version is covered: no reactivation, no key re-entry, works offline. The full guide is Release coverage; the client side is two steps.
Tell the build which release it is. The Keyright.NET package ships MSBuild targets that read these properties and embed them in the assembly (set them from your release pipeline, e.g. -p:KeyrightCoverageVersion=2.4.0):
<PropertyGroup>
<KeyrightCoverageVersion>2.4.0</KeyrightCoverageVersion> <!-- this build's version -->
<KeyrightCoverageEntitlementDate>2026-10-01</KeyrightCoverageEntitlementDate> <!-- the date coverage is judged against; a hotfix uses its base release's date -->
<KeyrightCoverageChannel>stable</KeyrightCoverageChannel> <!-- or beta -->
<KeyrightRequireCoverage>true</KeyrightRequireCoverage> <!-- enforce in this build; fails the build (KR0001) if the date is missing -->
</PropertyGroup>
Check at startup. Activation already delivers and caches the signed coverage block with the lease. On an enforcing build, ask once; the SDK decides from the cached block, silently refreshes the license if that says “no”, and never throws for network trouble:
using Keyright.Coverage;
if (CoverageBuildInfo.Current.RequireCoverage)
{
CoverageCheckResult check = await License.CheckCoverageAsync(ct: ct); // this build's version + entitlement date
if (!check.IsCovered) // Covered and NotConfigured both pass
{
// check.Status: NotCovered (check.Reason = WindowLapsed → renew, OutsideVersionRange → upgrade)
// or UnavailableOffline (nothing cached and no network → import a coverage file)
// CoverageMessaging.Build(...) gives you the sentence and the single action (renew / upgrade / import).
RunReadOnlyOrBlock(check);
}
}
bool can30 = License.IsCovered("3.0.0"); // for your own update banner — local, no network
CoverageInfo cov = License.GetCoverage(); // the terms, support-until date, enforced / unenforced
For air-gapped customers, import the coverage file you exported from the dashboard with License.ImportCoverageBlock(json). Your own download page asks the same question with POST /v1/coverage.
Troubleshooting
- Activation says the key wasn’t recognized. The most common cause is a product mismatch: the key was issued for a different product (or a different tenant) than the
Productslug your client is configured with. The server scopes activation by product, so a valid key foracme-appwon’t activate a client initialized withProduct = "other-app". Confirm the slug in Step 5 matches the product you issued the key under. - Everything reads as Free. That’s the design — the SDK fails closed on any verification problem instead of throwing. Read
info.Status(e.g.NoLicense,SignatureInvalid,Expired,MachineMismatch,Revoked) andinfo.Messageto see which one. ASignatureInvalidalmost always means the embedded public key doesn’t match the tenant that signed the key. - A time-limited license suddenly won’t validate. If the system clock is moved backward beyond
ClockTamperToleranceHours(default 24h) on a trial or subscription, the SDK treats it as clock tampering and returns statusClockTampered. It does not stick — set the correct time and validation recovers on the next call. Perpetual licenses are never subject to this check.
See also
- Getting started — the condensed vendor path and go-live checklist.
- Release coverage — which releases a license covers, and the SDK calls behind Step 11.
- Self-service free trials — enabling trials and the customer-facing flow.
- Admin dashboard — every screen behind these steps.
- HTTP API reference — the full endpoint map.