Skip to content

Start here

Getting started

The end-to-end vendor path for both Keyright Cloud and self-hosted — get an issuing service, create a product and signing key, put the SDK in your app, issue a license, verify it in-app, and go live.

This is the end-to-end path for a vendor adding licensing to an app: get an issuing service, create a product and a signing key, embed the SDK in your product, issue a license, verify it in-app, and go live. It runs the same whatever language you ship in — step 3 links to the SDK for yours (or the raw-HTTP path).

Keyright comes in two deployment modes, and every step below shows both. Throughout, $BASE is your issuing service URL and $TOKEN is your admin credential, sent in the X-Admin-Token header.

Pick your path

Keyright Cloud — we host the issuing service; you sign up and start issuing keys. Self-hosted — you deploy the service in your own infrastructure. The part you build is the same for both: the SDK code, the signed license format, and the public key you ship are identical. Only the service URL your app points at and the admin credential you manage with differ.

Keyright CloudSign up at keyright.delta1labs.comWe host the issuing service$BASE = keyright.delta1labs.com$TOKEN = team session tokenSelf-hostedYou deploy the issuing servicein your own infrastructure$BASE = your instance URL$TOKEN = KEYRIGHT_ADMIN_TOKENYour product + Keyright SDK — identical either waysame code, license format & public key; only ServiceUrl + admin token differ
Both paths meet at the same app code — pick a mode, set `$BASE` and `$TOKEN`, and the rest of this guide is shared.

1. Get your issuing service

Cloud: create a workspace at keyright.delta1labs.com and sign in. Your workspace is provisioned with a signing key automatically, so you can go straight to step 2. Set $BASE=https://keyright.delta1labs.com; your $TOKEN is a team session token — minted when you sign in, and you can invite teammates (with their own tokens) under Settings → Team.

Self-hosted: pull the public container image, point it at an empty PostgreSQL database, give it the secrets it needs (an admin token, a key-encryption secret) and your Keyright license key, then start it — it builds its own schema on first boot and answers /health when it’s ready. Set $BASE to your instance’s URL (locally that’s http://localhost:8080), and $TOKEN to the KEYRIGHT_ADMIN_TOKEN you generated. The full walkthrough is the Deployment guide, and your instance needs a valid key to run — see Licensing & editions.

Everything from here is the same for both modes; only $BASE and $TOKEN change.

Once you’re signed in you land on the dashboard Home, which tracks your progress through exactly the steps below:

The Keyright dashboard Home: a left sidebar (Home, Licenses, Customers, Products, Releases & coverage, Usage & credits, Resellers, Developers, Activity, Settings) with a Self-hosted badge beside the workspace name, four summary tiles — Active licenses 4 of 6, Active devices 4, Expiring in 30 days 1, Revoked 1 — and a 'Finish setting up' checklist whose items are Create your first product, Add an edition (seats and features), Issue a license, Add the SDK to your app, See your first device activate, and Connect your store, alongside a licenses-issued chart and a recent-activity feed.
The dashboard Home after sign-in — the "Finish setting up" checklist mirrors steps 2–4 of this guide.

2. Create a product (and an edition), then get a signing key

Create the product first — you can’t generate a signing key or issue a license for a product that doesn’t exist. From the dashboard (Products → New product, then Editions → Add edition — the dashboard calls tiers editions; the API still says tiers), or via the API:

The Products list showing one product, Acme App (slug acme-app): the Editions column lists enterprise, pro and starter as badges, the Licenses column reads 4 / 6, the Trial column shows a '14-day trial · pro' badge, and the Signing key column shows a 'Per-product key' badge — with a New product button in the top-right.
Step 2 starts at Products → New product. Each product lists its editions, trial and signing key at a glance.
# create the product
curl -X POST $BASE/admin/products -H "X-Admin-Token: $TOKEN" -H "content-type: application/json" \
  -d '{"name":"Acme App","slug":"acme-app"}'

# add a tier/edition (seat count + an entitlements template your app reads at runtime)
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 "unlimited" for limits). Quotas (active licenses, product count) follow the tenant’s plan on Cloud; self-hosted paid editions are unlimited. If you sell updates (a year of updates, one major version, a subscription…), give the edition a coverage policy before you issue — see Release coverage.

The product’s Overview tab gathers all of this — the editions with their seat counts and entitlements, the trial, and the signing key you’ll embed next:

The Acme App product page on its Overview tab (tabs: Overview, Editions, Trials, Releases, Pricing, Signing key, Messages). A right-hand column shows a Trial card (14-day trial on the pro edition, 1 seat), a Signing key card labelled 'Per-product key — Licenses for this product are signed with their own key. Embed its public key in the app.', and an Editions card listing enterprise (25 seats; entitlements export, sso, airgap, max-projects), pro (3 seats; export, max-projects) and starter (1 seat; max-projects).
The product page: each edition carries its seat count and entitlements (export, max-projects…), and the Per-product signing key card holds the public key you embed in step 3.

Get a signing key. Licenses and leases are signed with an RSA key whose private half stays server-side; your app embeds only the public key. A key is either per-product or tenant-wide (shared by all your products).

  • Per-product (recommended): curl -X POST $BASE/admin/products/acme-app/signing-key -H "X-Admin-Token: $TOKEN" — the response includes the product’s signingPublicKey. In the dashboard it’s on the product’s Signing key tab.
  • Tenant-wide: read it from Developers → Keys, or GET $BASE/admin/public-key (also GET $BASE/admin/account → integration.signingPublicKey).

GET /admin/public-key and GET /admin/account return the tenant key, which is null until a tenant key exists. On a fresh self-hosted instance with no tenant key set, generate a per-product key (above) rather than embedding a null value. A Cloud workspace already has a tenant key.

Copy the base64 public key of whichever key signs that product’s licenses — you’ll paste it into the SDK in step 3. The dashboard opens in a calm Basic view that covers exactly these steps; switch to the Intermediate or Advanced view when you need everything else.

3. Put the SDK in your product

This is the heart of the integration: embed the SDK, initialize it with your product slug and public key, and gate a feature. The code is identical on Cloud and self-hosted — the only thing that changes between modes is ServiceUrl (your $BASE) and, server-side, which admin token issued the key. The license format and the embedded public key don’t change.

The dashboard writes the first version of this code for you: Developers → Quickstart picks your product and language and prefills the snippet with the product slug, its public key, and this instance’s URL — copy it straight in.

The Developers → Quickstart screen titled 'Add the SDK to your app', with the Acme App product selected in a dropdown and language tabs for .NET, Node.js, Python and Java. Under the .NET tab it shows the install command 'dotnet add package Keyright.NET' and a copyable snippet prefilled with Product = acme-app, a long PublicKeyBase64 string and ServiceUrl, calling ActivateAsync then Validate and gating on License.IsEnabled(\
Step 3's snippet generator (Developers → Quickstart): choose the product and language and it prefills the slug, public key and service URL for you.

Here’s that minimal integration — install, initialize with the product slug + public key + service URL, activate online, then gate a feature — in each supported language. Set the service URL to your $BASE (the hosted URL on Cloud, your instance URL self-hosted); everything else — the SDK calls, the signed license/lease format, the embedded public key — is identical on Cloud and self-hosted.

.NET — Keyright.NET on NuGet (multi-targets netstandard2.0 and net8.0, so it runs on .NET Framework 4.8 and .NET 6–10):

dotnet add package Keyright.NET
using Keyright.Client;

static readonly KeyrightClient License = KeyrightClient.Initialize(new KeyrightOptions
{
    Product         = "acme-app",                        // the product slug from step 2
    PublicKeyBase64 = "MIIBIjANBgkq...",                 // the public key from step 2 — not a secret; it only verifies
    ServiceUrl      = "https://keyright.delta1labs.com", // Cloud: the hosted URL. Self-hosted: your instance URL. Omit for offline-only.
});

// When the customer enters a key, activate online: binds this machine, consumes a seat, caches a signed lease.
LicenseInfo info = await License.ActivateAsync(customerEnteredKey, ct);

// Every later launch verifies offline against the cached lease — no network round-trip.
LicenseInfo offline = License.Validate();

// Gate a paid feature on an entitlement (not on the edition), so tier changes need no new code.
if (info.IsPaid && License.IsEnabled("export"))
    ShowExportCommand();

Node.js — keyright on npm (CommonJS, zero dependencies):

npm install keyright
const { KeyrightClient } = require('keyright');

const license = KeyrightClient.initialize({
  product:         'acme-app',                          // the product slug from step 2
  publicKeyBase64: 'MIIBIjANBgkq...',                   // the public key from step 2 — not a secret
  serviceUrl:      'https://keyright.delta1labs.com',   // Cloud: the hosted URL. Self-hosted: your instance URL.
});

// When the customer enters a key, activate online (binds the machine, consumes a seat, caches a signed lease).
const info = await license.activate(customerEnteredKey);

// Later launches verify offline against the cached lease — synchronous, never throws.
const { info: offline } = license.validate();

// Gate a paid feature on an entitlement.
if (info.isPaid && info.entitlements.isEnabled('export')) {
  showExportCommand();
}

Python — keyright on PyPI (Python 3.8+):

pip install "keyright>=1.1.4"
from keyright import KeyrightClient, KeyrightOptions

license = KeyrightClient.initialize(KeyrightOptions(
    product="acme-app",                              # the product slug from step 2
    public_key_base64="MIIBIjANBgkq...",             # the public key from step 2 — not a secret
    service_url="https://keyright.delta1labs.com",   # Cloud: the hosted URL. Self-hosted: your instance URL.
))

# When the customer enters a key, activate online (synchronous).
info = license.activate(customer_entered_key)

# Later launches verify offline against the cached lease; validate() returns (info, source).
offline, source = license.validate()

# Gate a paid feature on an entitlement.
if info.is_paid and info.entitlements.is_enabled("export"):
    show_export_command()

Java — com.delta1labs:keyright on Maven Central (Java 17+, no runtime dependencies):

<dependency>
  <groupId>com.delta1labs</groupId>
  <artifactId>keyright</artifactId>
  <version>1.1.4</version>
</dependency>
import com.delta1labs.keyright.KeyrightClient;
import com.delta1labs.keyright.KeyrightOptions;
import com.delta1labs.keyright.LicenseInfo;

KeyrightClient client = KeyrightClient.initialize(
    new KeyrightOptions()
        .product("acme-app")                                 // the product slug from step 2
        .publicKeyBase64("MIIBIjANBgkq...")                  // the public key from step 2 — not a secret
        .serviceUrl("https://keyright.delta1labs.com"));     // Cloud: the hosted URL. Self-hosted: your instance URL.

// When the customer enters a key, activate online (synchronous).
LicenseInfo info = client.activate(customerEnteredKey);

// Later launches verify offline against the cached lease.
LicenseInfo offline = client.validateInfo();

// Gate a paid feature on an entitlement.
if (info.isPaid() && info.entitlements.isEnabled("export")) {
    showExportCommand();
}

Any language (raw HTTP, no SDK) — the runtime API is plain JSON over HTTP, so Go, Rust, C++, PHP, Ruby… talk to it directly. Activate a machine, then verify the signed lease yourself:

curl -s -X POST "$BASE/v1/activate" -H "content-type: application/json" \
  -d '{"key":"LIC-9F3A...","machineId":"<your stable per-machine id>","product":"acme-app"}'
# -> { "status":"ok", "seats":3, "used":1, "lease":"{...signed lease JSON...}" }

Store the lease string verbatim and verify its RSA signature against your embedded public key — rebuild the canonical krlease1|… payload and check it with PKCS#1 v1.5 / SHA-256 — before trusting it. Once it verifies, read tier for the edition and entitlements for per-feature gates, and re-validate periodically (POST /v1/validate) to pick up revocations. Fail closed on any non-ok status or a lease that doesn’t verify. The full byte-level recipe (with a Go example you can port) is Use the HTTP API directly.

Every SDK verifies the signature against your embedded public key, checks machine binding and expiry, caches the lease locally, and re-activates automatically before it expires. All of them fail closed to the free/unlicensed state if verification fails or the key is revoked — and because the five clients verify the same signed format, a mixed-language product line shares one tenant and one public key.

For the complete per-language walkthrough — deactivation, offline/air-gapped leases, trials, node-locking, key rotation and release coverage — follow the full guide for yours:

LanguagePackageGuide
.NETKeyright.NET (NuGet).NET SDK integration
Node.jskeyright (npm)Node.js SDK integration
Pythonkeyright (PyPI)Python SDK integration
Javacom.delta1labs:keyright (Maven)Java SDK integration
Anything else (Go, Rust, C++, PHP…)—Use the HTTP API directly

What’s different Cloud vs self-hosted? Only the base URL and the admin token. Set ServiceUrl to the hosted URL for Cloud or to your instance’s URL for self-hosted; everything else — the SDK calls, the license and lease format, embedding the public key — is the same. See Feature gating for entitlements and limits in depth.

4. Issue a license and verify it in-app

Issue a paid license from Licenses → Issue license in the dashboard, or over the API (omit id to auto-generate the key; include it to import an existing one):

The Licenses list for acme-app with an Issue license button top-right and search, status-filter, Export and Import CSV controls. Six rows, each with the licensee, license key, product·edition, seats used/total, expiry and a status pill: Stark Industries (pro, 2/3, perpetual, active), Wayne Enterprises (enterprise, 0/10, perpetual, revoked), Umbrella Ltd (pro, 0/3, expired Mar 1 2026), Initech LLC (pro, 0/1, expiring 'in 10 days' Oct 22 2026), Globex Corporation (enterprise, 1/25, active, expires Jun 1 2027) and Acme Inc. (pro, 1/3, perpetual, active).
Step 4: issue from Licenses → Issue license. Each row shows seats used/total, expiry and status — active, revoked, expired or expiring soon.
curl -X POST $BASE/admin/licenses -H "X-Admin-Token: $TOKEN" -H "content-type: application/json" \
  -d '{"licensee":"Acme Inc.","product":"acme-app","tier":"pro","seats":3,"email":"owner@acme.com"}'
# -> HTTP 201
# { "id":"LIC-XXXX...", "licensee":"Acme Inc.", "product":"acme-app", "tier":"pro",
#   "seats":3, "used":0, "expiryUtc":null, "revoked":false, "trial":false, "createdAt":"..." }
# The "id" is the key you give the customer.

The customer pastes that key into your app, which calls ActivateAsync (online) — it binds the machine, consumes a seat, and caches a signed lease so later Validate() calls succeed offline. Machine binding is automatic: Activate takes only the key; the SDK derives a stable machine id from the host, so a single-seat key can’t run on two machines (the raw POST /v1/activate body carries { key, machineId, product } — the SDK fills machineId for you). What you read back from LicenseInfo:

  • IsPaid / IsTrial — whether it’s a paid or trial license (both activate through the same path).
  • IsEnabled("flag") / GetLimit("name", fallback) — entitlement checks that fail closed.
  • DaysRemaining / ExpiryUtc — for a “27 days left” badge; StatusBadge for a label like “Pro Trial”.

As each machine activates, it appears on that license’s Devices tab, consuming a seat — you can see which devices are bound, free a seat, and read the entitlements the license carries:

The Stark Industries license page (key LIC-58D5ECC3A42E35D84311, Acme App · pro · perpetual, contact jarvis@stark.com) with Renew, Add seats, License file and Send portal link actions and Overview, Devices, Coverage, Credits and History tabs. A 'What the customer sees' banner reads 'Your license covers versions released on or before Oct 12, 2027' above a coverage checker. The Devices section shows 2 of 3 seats in use — machines WORKSTATION-STAR and DESKTOP-STARK-01, both online, each with a lease renewing by Oct 26 2026 and a Free seat button. A Details panel lists edition pro, 3 seats (2 in use), perpetual term and entitlements export and max-projects: 10, and a Danger zone offers Transfer ownership and Revoke.
After the customer activates, their machine shows up on the Devices tab (2 of 3 seats in use) — alongside the coverage sentence, entitlements and renew/revoke actions.

Full activation and offline paths are in Activation & seats and Offline & air-gapped.

Getting a trial key. If you enabled a self-service trial on the product (Products → Trials), your app obtains a trial key by calling the public trial endpoint directly — there’s no SDK wrapper for issuing a trial:

curl -X POST $BASE/v1/trial -H "content-type: application/json" \
  -d '{"product":"acme-app","email":"user@customer.com","company":"Customer Ltd"}'
# -> { "ok":true, "product":"acme-app", "trial":true, "emailed":true, "expiresUtc":"...", "days":14, "created":true }

The endpoint emails the trial key to the address (there’s no key in the response — it needs SMTP configured on the instance); the customer then enters that key and passes it to ActivateAsync() like any other key. It activates and validates exactly like a paid key; info.IsTrial is true and DaysRemaining counts down. Full flow: Self-service trials.

5. Go live

Both modes

  • The public key embedded in the shipping build matches the key that signs that product’s licenses.
  • The Product slug in the SDK matches the product you issue licenses for (the server scopes activation by product).
  • Decide offline vs online (or both): an offline file for air-gapped/enterprise, online activation for seat enforcement and revocation — see Offline & air-gapped.
  • Seats and expiry set correctly on the edition/license; test hitting the seat limit.
  • Test the revocation path (POST /admin/licenses/{id}/revoke) — the app should drop to free on the next lease refresh. See Revoke, renew & expiry.
  • If you enforce release coverage, backfill every license issued before coverage (Releases & coverage → Enforcement readiness) before shipping a KeyrightRequireCoverage build, and set KeyrightCoverageVersion / KeyrightCoverageEntitlementDate from your release pipeline — Release coverage.

Cloud only

  • If you sell via a store / Merchant-of-Record, wire fulfillment to POST /webhooks/fulfill/{slug} so paid orders auto-issue keys — Store fulfillment.
  • Confirm your plan covers your expected active-license and product counts (upgrade under Settings → Plan & billing).

Self-hosted only

  • Have a backup plan for the database and your key-encryption secret — losing that secret means losing every signing key. Back up your own instance license too.
  • Have a signing-key rotation plan in place.
  • Keep a valid instance license installed — a self-hosted instance is fail-closed and stops serving runtime calls once its own license lapses past grace (Licensing & editions).

The API at a glance

AreaEndpointAuthPurpose
RuntimePOST /v1/activatenoneActivate a key on a machine → signed lease. Body: { key, machineId, product }
RuntimePOST /v1/validatenoneValidate/refresh without consuming a seat. Body: { key, machineId, product }
AdminGET/POST /admin/products, /admin/products/{slug}/tiersadmin tokenManage products & tiers
AdminPOST /admin/products/{slug}/signing-keyadmin tokenGenerate/rotate a product’s signing key → signingPublicKey
AdminPOST /admin/licenses, /admin/licenses/{id}/{revoke,renew,transfer,deactivate,offline-lease}admin tokenIssue & manage licenses
AdminGET /admin/licenses, /admin/overview, /admin/licenses/{id}/activationsadmin tokenReporting
AdminGET /admin/self-licenseadmin tokenSelf-hosted only: this instance’s license status (edition, phase, days left)
AdminGET/POST /admin/teamowner tokenTeam members
RuntimePOST /v1/coveragenone (license key)Is version X covered for this license? — the building block of your own download gate
AdminPOST /admin/products/{slug}/releases, /admin/licenses/{id}/coverage/{preset,grants}, /admin/products/{slug}/coverage/{readiness,backfill,export,migrate}admin tokenRelease coverage: registry, terms, backfill, air-gap export
PlatformGET/POST /platform/tenants, /platform/plans, /platform/tenants/{id}/signing-keyplatform tokenMulti-tenant operator
AuthPOST /auth/login → /auth/mfa, /auth/session, /auth/sso—Dashboard sign-in → session token

The runtime bodies use machineId (a stable per-machine fingerprint) and product (the product slug) alongside the license key — unrecognized field names are treated as an unknown key. Full request/response details per language are in the SDK and HTTP API guides; coverage endpoints are documented in Release coverage.

The full endpoint map is in the HTTP API reference.