Guide
Activation & deactivation
Every activation and deactivation path for trial, annual (term) and perpetual licenses — online, offline/air-gapped, and vendor-side — and exactly what your app sees in each case.
This page walks through every activation and deactivation path a vendor hits in practice: trial, annual (term) and perpetual licenses, activated and deactivated online, offline / air-gapped, and vendor-side. The code snippets are C# (Keyright.NET), but the SDK shape is identical in the Node, Python and Java packages — see the runnable samples repo.
The three license shapes
A license carries a tier, a seat count, an optional expiry, and an optional trial flag. Those combine into the three shapes your customers see:
| Shape | ExpiryUtc | IsTrial | What your app shows |
|---|---|---|---|
| Perpetual | null | false | “Licensed — perpetual” |
| Annual / term | a date | false | “Licensed — expires 31 Dec 2027” |
| Trial / evaluation | a date | true | “Evaluation — expires 31 Dec 2027 (N days left)” |
Your app reads all of this off the LicenseInfo returned by Activate/Validate — you never parse a key string yourself:
var info = await client.ActivateAsync(licenseKey);
if (info.Status != LicenseStatus.Valid) // fail closed
return ShowUnlicensed(info.Message);
string banner = info.IsTrial
? $"Evaluation — expires {info.ExpiryUtc:d} ({info.DaysRemaining} days left)"
: info.ExpiryUtc is null
? "Licensed — perpetual"
: $"Licensed — expires {info.ExpiryUtc:d}";
Activation (online)
ActivateAsync(key) sends the machine’s fingerprint and the key to the issuing service. The service checks the seat count, binds the license to this machine, and returns a signed lease that the SDK caches locally. From then on Validate() works offline against that cached lease (see Security & the lease model).
Re-activating the same machine is idempotent — it refreshes the lease and never consumes a second seat. Calling ActivateAsync at every startup (when online) is the recommended pattern: it silently keeps the lease fresh.
Trial activation
A trial key activates exactly like a paid key — same call, same seat model — it just comes back with IsTrial == true and an expiry:
var info = await client.ActivateAsync(trialKey);
// info.IsTrial == true, info.ExpiryUtc == the trial end, info.DaysRemaining counts down
Show the evaluation banner and gate whatever you want behind info.IsTrial. When the trial date passes, activation/validation returns LicenseStatus.Expired and you fall back to your unlicensed state. Trials are also duration-from-first-activation aware and clock-tamper aware — a customer can’t win back time by turning the clock back (see the security page). Trials are usually self-served from your own site; see Self-service free trials.
Perpetual activation
A perpetual license has no expiry. Activate once; ExpiryUtc is null, so your “perpetual” branch runs. The lease still has a short TTL and is refreshed on the next online activation — the license never expires, but the offline lease is still a short-lived grace token (that’s what keeps a perpetual license from being copied to unlimited machines offline).
Annual / term activation
Identical call; ExpiryUtc is the paid-through date. Use info.IsExpiringSoon() / info.DaysRemaining to nudge customers before renewal. After the date, activation returns Expired. Renewing is a vendor-side change to the license (/admin/licenses/{id}/renew) — the customer keeps the same key and simply re-activates to pick up the new expiry.
When there are no seats left
If a new machine tries to activate a license whose seats are all used, the service returns LicenseStatus.SeatLimit — surface a clear “all seats in use — free one first” message. (Again, the same machine re-activating never hits this.)
Deactivation
Customer-initiated (online)
DeactivateAsync(key) frees this machine’s seat and clears the local lease, so the seat is immediately available elsewhere. This is how a customer moves machines:
await client.DeactivateAsync(key); // on the old machine -> seat freed
// ...then on the new machine:
await client.ActivateAsync(key); // takes the freed seat
This works the same for perpetual, annual and trial licenses. After deactivation, Validate() returns NoLicense on that machine.
Vendor-side (force-deactivate)
When a machine dies, is re-imaged, or a customer can’t get to it, you free the seat from your backend — no client needed:
curl -X POST "https://keyright.delta1labs.com/admin/licenses/<KEY>/free-seat" \
-H "X-Admin-Token: $KEYRIGHT_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"machineId":"KRM1:..."}'
You can see and free a license’s machines from the dashboard’s Licenses view, or via POST /admin/licenses/{id}/free-seat (Support+) / /deactivate (Admin+). The machine’s next online Validate() sees the seat is gone.
Offline / air-gapped activation
For a machine that can’t reach the service at all, the vendor issues a long-lived signed lease bound to that one machine. It works identically for trial and paid licenses.
-
On the offline machine, read its ID:
var machineId = MachineFingerprint.Current().ToBoundString(); // KRM1:...(or run
keyright machine-id). Send that string to the vendor. -
The vendor (with connectivity + an admin token) mints a lease for that machine — default TTL 365 days:
curl -X POST "https://keyright.delta1labs.com/admin/licenses/<KEY>/offline-lease" \ -H "X-Admin-Token: $KEYRIGHT_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"machineId":"KRM1:...","days":365}' > offline-lease.jsonThis confirms a seat for that machine (so offline activations still count against the seat limit — you can’t mint unlimited offline leases past the seat count).
-
Back on the offline machine, import it — no network involved:
client.ImportOfflineLease(File.ReadAllText("offline-lease.json")); var info = client.Validate(); // Valid, fully offline
Because the lease is bound to that machine’s fingerprint, the file is useless on any other machine (it returns MachineMismatch). See the security page for why this holds even offline.
Fully offline, no service at all
If you never want the machine (or the vendor) to touch a network, sign a standalone offline license file with the CLI and ship it with the product:
keyright license --sign --licensee "Acme Corp" --product yourapp --tier pro \
--private-key yourapp.private.json --expiry 2027-12-31 --out license.json
The client validates it purely offline via Validate() (it’s discovered by path, the KEYRIGHT_LICENSE env var, or the default app-data location). There’s no seat tracking in this mode — it’s the right fit for genuinely air-gapped or embedded deployments.
A note on offline deactivation
There is no client-generated “proof of deactivation” today, so an air-gapped machine can’t hand back its seat by itself. Free it vendor-side (force-deactivate, above) once your backend has connectivity, or issue offline leases with a TTL short enough that a decommissioned machine’s seat returns on its own when the lease lapses. See the samples repo.