SDK integration
Use the HTTP API directly (any language)
No SDK for your language? Call the Keyright runtime API over HTTP: activate a machine, verify the signed lease offline yourself (canonical payload + RSA/SHA-256), deactivate, and handle air-gapped machines — with copyable examples you can port to Go, Rust, C++, PHP and more.
Keyright ships official SDKs for .NET, Node.js, Python, and Java — if you’re on one of those, use it. Each SDK implements everything on this page (activation, offline lease verification, caching, node-locking, revocation) and is the supported path. See the .NET SDK integration guide for the shape they all follow.
This page is for everyone else — Go, Rust, C++, PHP, Ruby, Swift, or anything without a Keyright SDK. The runtime API is plain JSON over HTTP, and the license/lease formats are versioned, language-independent specs. You can talk to it directly. This page is both a how-to and the exact byte-level reference you need to verify a lease correctly in your language.
The whole model in two moves
Everything a client does reduces to two things:
- Activate — POST a license key plus a stable machine id, and get back a short-lived, RSA-signed lease bound to that machine. That’s the online step: it consumes a seat and proves the key is valid right now.
- Verify the lease offline — check the lease’s signature against the vendor’s public key yourself, then check product/machine/expiry. Once you can do this, your app keeps working with no network until the lease’s grace window ends. If you only ever call online, you can skip verification and trust the
statusfield — but then a network outage locks your users out, so most clients verify.
Both runtime endpoints (/v1/activate, /v1/validate) and /v1/free-seat are no-auth — no token, called directly by your app. They’re rate-limited per client IP (a generous per-minute window; a 429 carries Retry-After: 60).
$BASE below is your vendor’s issuing service URL, e.g. https://keyright.delta1labs.com.
Activate
POST JSON to /v1/activate. The only required fields are key, machineId, and product; the rest are optional metadata the vendor sees in their dashboard.
curl -s -X POST "$BASE/v1/activate" \
-H "content-type: application/json" \
-d '{
"key": "LIC-9F3A...",
"machineId": "b1946ac92492d2347c6235b4d2611184",
"product": "acme-app",
"os": "linux",
"hostname": "build-07",
"appVersion": "2.4.0"
}'
A successful response:
{
"status": "ok",
"seats": 3,
"used": 1,
"license": {
"licensee": "Acme GmbH",
"product": "acme-app",
"tier": "pro",
"expiryUtc": "2027-01-01T00:00:00.0000000Z",
"trial": false,
"revoked": false
},
"lease": "{\"key\":\"LIC-9F3A...\",\"machineId\":\"b1946ac9...\",\"product\":\"acme-app\",\"licensee\":\"Acme GmbH\",\"tier\":\"pro\",\"licenseExpiryUtc\":\"2027-01-01T00:00:00.0000000Z\",\"leaseExpiresUtc\":\"2026-10-11T09:14:22.1030000Z\",\"entitlements\":{\"export\":\"true\",\"max-projects\":\"10\"},\"signature\":\"Base64Sig==\"}"
}
Two things to notice:
leaseis a JSON string — the whole signed lease document, serialized, as one field inside the response. You store this string verbatim (it’s your offline proof), and you parse it as its own JSON object to verify it. Don’t reformat it before storing.- The
licenseblock is a convenience summary. Do not trust it for gating — it isn’t individually signed. The signed lease is the source of truth; the summary is just for display.
Handling status
status is the field you branch on. Values:
status | Meaning | What to do |
|---|---|---|
ok | Activated; lease is present | Verify and cache the lease; unlock features |
not_found | Key unknown, or wrong product | Show “key not recognized”; stay in free mode |
revoked | Key was revoked | Drop to free mode |
expired | License (or trial) has expired | Drop to free mode; prompt to renew |
seat_limit | All seats for this key are in use | Tell the user to free a seat on another machine |
Only ok returns a lease. Fail closed: treat any non-ok status, any non-2xx HTTP code, a 429, a malformed body, or a lease that fails verification as not licensed. Never fall through to an unlocked state on error.
Seats and idempotency
Seats are enforced server-side. The key’s seats count is the ceiling; used tells you how many machines currently hold a seat. Re-activating a machine that’s already bound to the key does not burn another seat — the server keys on (key, machineId), so calling /v1/activate again for the same machine is idempotent and just refreshes the lease. This is why the machine id must be stable (next section). If it changes, the machine looks new and consumes a fresh seat, and the user hits seat_limit.
The machine id
machineId is your identifier for the device — you choose it. The official SDKs compute a salted node-lock fingerprint (their internal KRM1 scheme) from hardware and OS signals, but the server treats machineId as an opaque string. A no-SDK client does not need to match that scheme. You just need an id that is:
- Stable across restarts, updates, and reboots. If it changes, the machine looks new, burns a seat, and eventually trips
seat_limit. This is the single most important property. - Per-machine, so seat counting is meaningful.
- Not trivially forgeable by an end user who wants to clone one activation across many machines (a hash, not a value they can type in).
A good recipe: gather one or two stable OS/hardware identifiers (a machine GUID, a primary MAC, a disk serial, or a systemd/machine-id), concatenate them with a constant salt string of your own, hash with SHA-256, and hex- or base64-encode the digest. Compute it once, and reuse the exact same string every time you activate, validate, verify, or free a seat. Store it alongside the cached lease so you can compare later.
machineId = hex( sha256( "acme-app-salt|" + stableHardwareId ) )
Keep it consistent forever for a given install. That’s all the server needs.
Verify the lease offline
This is the part with no shortcuts. The lease is signed with the vendor’s private RSA key, which never leaves their server. You verify it with the matching public key, which the vendor gives you (dashboard Integration tab, or GET $BASE/admin/public-key with an admin token) as a base64 SubjectPublicKeyInfo (SPKI) string. That key is not a secret — it can only verify, never sign — so you embed it in your binary.
Verification is: rebuild the exact bytes that were signed, then RSA-verify the signature over them. The signed bytes are not the lease JSON — they’re a canonical string assembled from specific fields in a specific order.
The canonical payload — get this byte-exact
The signed string is (spec version krlease1):
krlease1|<key>|<machineId>|<product>|<licensee>|<tier>|<licenseExpiryUtc-or-empty>|<leaseExpiresUtc>
Then, conditionally and in this exact order, two suffixes are appended:
|ent=<entitlements>— appended only if the lease has a non-emptyentitlementsobject. The entitlements are serialized deterministically: keys sorted by ordinal (byte/codepoint) order, each rendered askey=value, comma-joined. E.g.{"max-projects":"10","export":"true"}→export=true,max-projects=10.|trial— appended only if the lease’strialfield istrue. Nothing is appended when it’s false.
The whole string is UTF-8 encoded, and the signature is RSA PKCS#1 v1.5 over SHA-256, base64-encoded in the lease’s signature field.
This ordering bug is real — mind it.
|ent=comes before|trial. Get the order wrong, drop a conditional suffix that should be present, add one that shouldn’t be, or reformat a date, and the rebuilt bytes differ by one character — and the signature silently fails to verify. There is no partial match. This exact ordering bug bit the SDKs during development; it’s why the order is spelled out so precisely. When a lease you know is good won’t verify, the canonical string is where to look first.
Critical details that trip people up:
- Use the date strings verbatim from the JSON.
licenseExpiryUtcandleaseExpiresUtcare round-trip ISO-8601 strings like2026-10-11T09:14:22.1030000Z(7 fractional digits). Do not parse and re-serialize them for the canonical string — copy the exact characters from the lease. (You parse them separately, later, only to compare against “now”.) licenseExpiryUtcmay be absent/null for a perpetual license. In that case put the empty string in that slot — the two|separators around it still appear:...|<tier>||<leaseExpiresUtc>.- Empty entitlements = no
|ent=at all. A lease with no entitlements signs over the exact same bytes as one issued before entitlements existed. Only append|ent=when the object is present and non-empty.
Concrete example (Go — port this to your language)
Go’s standard library covers all of it. The logic ports directly to Rust (rsa + sha2), PHP (openssl_verify with OPENSSL_ALGO_SHA256), Ruby (OpenSSL::PKey::RSA#verify), C++ (OpenSSL EVP_DigestVerify), etc. — only the crypto calls change.
package main
import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"sort"
"strings"
"time"
)
type Lease struct {
Key string `json:"key"`
MachineID string `json:"machineId"`
Product string `json:"product"`
Licensee string `json:"licensee"`
Tier string `json:"tier"`
LicenseExpiryUtc *string `json:"licenseExpiryUtc"` // pointer: may be null
LeaseExpiresUtc string `json:"leaseExpiresUtc"`
Entitlements map[string]string `json:"entitlements"`
Trial bool `json:"trial"`
Signature string `json:"signature"`
}
// Rebuild the EXACT bytes that were signed. Order is load-bearing.
func canonical(l *Lease) []byte {
var b strings.Builder
b.WriteString("krlease1|")
b.WriteString(l.Key + "|")
b.WriteString(l.MachineID + "|")
b.WriteString(l.Product + "|")
b.WriteString(l.Licensee + "|")
b.WriteString(l.Tier + "|")
if l.LicenseExpiryUtc != nil { // empty string when perpetual (null)
b.WriteString(*l.LicenseExpiryUtc)
}
b.WriteString("|")
b.WriteString(l.LeaseExpiresUtc)
// |ent= FIRST, only when non-empty. Keys sorted ordinal, k=v, comma-joined.
if len(l.Entitlements) > 0 {
keys := make([]string, 0, len(l.Entitlements))
for k := range l.Entitlements {
keys = append(keys, k)
}
sort.Strings(keys) // Go's sort.Strings is ordinal (byte-wise) — matches the spec
parts := make([]string, len(keys))
for i, k := range keys {
parts[i] = k + "=" + l.Entitlements[k]
}
b.WriteString("|ent=" + strings.Join(parts, ","))
}
// |trial LAST, only when true.
if l.Trial {
b.WriteString("|trial")
}
return []byte(b.String())
}
// publicKeyB64 is the vendor's base64 SubjectPublicKeyInfo (SPKI).
func VerifyLease(leaseJSON, publicKeyB64, expectedProduct, ourMachineID string, now time.Time) (*Lease, error) {
var l Lease
if err := json.Unmarshal([]byte(leaseJSON), &l); err != nil {
return nil, err // fail closed
}
// 1. Load the public key (SPKI).
der, err := base64.StdEncoding.DecodeString(publicKeyB64)
if err != nil {
return nil, err
}
pubAny, err := x509.ParsePKIXPublicKey(der)
if err != nil {
return nil, err
}
pub, ok := pubAny.(*rsa.PublicKey)
if !ok {
return nil, errors.New("not an RSA key")
}
// 2. Verify the signature over the canonical bytes (PKCS#1 v1.5, SHA-256).
sig, err := base64.StdEncoding.DecodeString(l.Signature)
if err != nil {
return nil, err
}
digest := sha256.Sum256(canonical(&l))
if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, digest[:], sig); err != nil {
return nil, errors.New("bad signature") // fail closed
}
// 3. Semantic checks — all must pass, or fail closed.
if l.Product != expectedProduct {
return nil, errors.New("wrong product")
}
if l.MachineID != ourMachineID {
return nil, errors.New("lease bound to a different machine")
}
leaseExp, err := time.Parse(time.RFC3339Nano, l.LeaseExpiresUtc)
if err != nil || !now.Before(leaseExp) {
return nil, errors.New("lease grace window elapsed") // time to re-activate online
}
if l.LicenseExpiryUtc != nil {
licExp, err := time.Parse(time.RFC3339Nano, *l.LicenseExpiryUtc)
if err != nil || !now.Before(licExp) {
return nil, errors.New("license expired")
}
}
return &l, nil // licensed — read l.Tier / l.Entitlements to gate features
}
The verification checklist (any language)
- Parse the lease JSON.
- Rebuild the canonical string exactly — mind
|ent=before|trial, the empty-string slot for a nulllicenseExpiryUtc, verbatim dates, and ordinal-sorted entitlement keys. - Base64-decode
signature, base64-decode the SPKI public key, and RSA-verify (PKCS#1 v1.5 / SHA-256). - Check
productequals your product. - Check
machineIdequals the machine id you stored at activation. - Check
leaseExpiresUtcis in the future (the offline grace window — when it passes, re-activate online). - Check
licenseExpiryUtc, if present, is in the future. - Fail closed on anything — a parse error, a bad signature, a mismatch, an expiry, a clock you can’t trust. Only a lease that passes every step is licensed.
Once it passes, read tier for the edition and entitlements for per-feature gates.
Deactivate a machine
To release the seat this machine holds (e.g. before uninstalling, or migrating to new hardware), POST to /v1/free-seat:
curl -s -X POST "$BASE/v1/free-seat" \
-H "content-type: application/json" \
-d '{"key":"LIC-9F3A...","machineId":"b1946ac9...","product":"acme-app"}'
Response: {"status":"ok"}. The seat is freed and used drops, so another machine can activate. Delete your local cached lease at the same time.
Re-validate and refresh
POST /v1/validatere-checks a key + machine against the current server state — it catches revocation and expiry that happened after activation — without consuming a new seat. It takes the same request body as/v1/activateand returns the same response shape (including a freshleasewhen stillok). Use it as a periodic health check.- Re-activating (
/v1/activate) on an already-bound machine is idempotent and refreshes the lease with a newleaseExpiresUtc. In practice this is what most clients do on a schedule.
Leases are short-lived — the default grace window is 14 days (the vendor may configure it). So an online client should re-activate (or validate) periodically — comfortably before the current lease’s leaseExpiresUtc — to pick up revocations and keep a fresh lease cached. Between refreshes, verify the cached lease offline on every start and gate on that.
Offline / air-gapped machines
For a machine that can never reach the service, the vendor mints a long-lived lease for you out of band. An operator calls (admin token required, so this is the vendor’s step, not your client’s):
curl -s -X POST "$BASE/admin/licenses/{licenseId}/offline-lease" \
-H "X-Admin-Token: $TOKEN" -H "content-type: application/json" \
-d '{"machineId":"b1946ac9...","days":365}'
which returns { "machineId": "...", "ttlDays": 365, "lease": "{...signed lease JSON...}" }. The days field defaults to 365 — a much longer TTL than an online lease’s grace window, since the machine will never refresh.
You obtain the machine id from the target machine first (compute it exactly as your client does, and hand it to the vendor), deliver the returned lease string to the machine by file or USB, and verify it with the exact same code as an online lease — same canonical payload, same signature check, same product/machine/expiry checks. An offline lease is an ordinary lease with a long leaseExpiresUtc; nothing about verification changes. (Offline leases minted this way are not marked trial, so no |trial suffix applies.)
A note on offline license files
Activation and leases are the online-then-cached path. Keyright also supports fully offline license files — a signed JSON document (license.json) with no activation round-trip at all. Its signed canonical payload is a different, simpler spec (version kr1):
<licensee>|<expiryUtc-or-empty>|<tier>[|trial][|kr1:<keyright-fields>]
where the optional kr1: block carries semicolon-joined product=, id=, machine=, td=, and ent= fields (same ordinal-sorted, comma-joined entitlement encoding). Same crypto — RSA PKCS#1 v1.5 / SHA-256, verified against the same SPKI public key. If your vendor ships you license files rather than using activation, verify them the same way: rebuild that canonical string and RSA-verify. Most no-SDK integrations only need the lease path above; reach for license-file verification only if your vendor distributes files directly.
See also
- Getting started — the vendor setup (create a product, get your public key, issue keys).
- Activation & lease lifecycle — how activation, leases, grace windows, and refresh fit together.
- Security & leases — the signing model, why the public key is safe to embed, and the canonical formats in depth.
- HTTP API reference — the full endpoint map.