Skip to content

Guide

.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 → Add 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.

Your workspace already has an RSA signing key, generated when the tenant was created. Every license and lease for every product in your workspace is signed with that key’s 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 dashboard’s Integration tab and copy the Signing public key — a base64 SubjectPublicKeyInfo string. Or fetch it 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 (e.g. pro, enterprise). Each tier carries a seat count and an entitlement template — the named flags and numeric limits that get baked into every license of that tier. Entitlements are how your app asks “is this feature allowed?” at runtime.

Add a tier from the product’s page in the dashboard (Add tier), 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 — enterpriseEnterprise, 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 — the Node.js, Python, and Java packages — so a mixed-language product line can share one Keyright tenant and one public key. 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://keyright.delta1labs.com",   // omit if you ship offline license files only

    // Optional: keep an old key valid during a rotation window (Step 9)
    // AdditionalPublicKeysBase64 = { "<previous public key>" },
});

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, ActivateAsync falls 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 (dashboard Licenses → offline lease, 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:

  1. From the dashboard. Licenses → Issue: pick the product and tier, set the licensee, seats, expiry, and contact email, and Keyright generates the key. The issue dialog also has a Trial toggle to mint the key as a time-limited trial (see Step 9 and Self-service free trials).
  2. 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.
  3. 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 ActivateAsync path as a paid one. Show the state straight off LicenseInfoinfo.IsTrial, info.StatusBadge (e.g. “Enterprise Trial”), info.ExpiryUtc, and info.DaysRemaining for 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 (default 1) so a swapped NIC or disk doesn’t lock the user out. To control what identifies a machine, implement IMachineComponents and set MachineComponents on 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 to Free (status Revoked) 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 PublicKeyBase64 and the outgoing one in AdditionalPublicKeysBase64 — 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

  1. In the dashboard, enable trials for acme-app (a short length is fine), then issue yourself a trial key.
  2. Run your app. Before activation, Validate() returns Free — your paid features stay locked.
  3. Call ActivateAsync with 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.
  4. Test offline. Disconnect the network and restart the app. Validate() still returns the licensed LicenseInfo from the cached lease — no server round-trip — until the lease’s grace window ends.
  5. Test revocation. Revoke the license, reconnect, and let the lease refresh; the app should drop back to Free.

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 Product slug your client is configured with. The server scopes activation by product, so a valid key for acme-app won’t activate a client initialized with Product = "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) and info.Message to see which one. A SignatureInvalid almost 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 status ClockTampered. 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