Concepts

Concepts

Products, plans, licenses, activations, statuses and the grace period: the vocabulary of every API and SDK.

Overview#

ObjectWhat it isID prefix
AccountYou, the software vendor. Has a Keyrail plan.acc_
UserA person who signs in to your dashboard. Roles owner, admin.usr_
ProductA piece of software you sell. Owns its Ed25519 signing keys. Its ID is public and embedded in your app.prd_
PlanA license template inside a product: kind, activation limit, entitlements, maximum major version.pln_
CustomerOne of your end users (email, name).cus_
LicenseWhat a customer bought: a license key, a status and validity dates.lic_
ActivationOne machine using a license.act_
IntegrationA connection to Paddle Billing or Lemon Squeezy.int_
API keyA secret key for the vendor API, kr_sk_ followed by 32 characters.key_

IDs are the prefix followed by 20 random lowercase Crockford base32 characters, for example lic_7k2m9q4x1c8d3f6h0j5n. API timestamps are ISO 8601 UTC strings (2026-12-01T00:00:00Z); inside certificates they are Unix seconds.

Products#

A product has a name, a keyPrefix for its license keys, a graceDays value (default 14) and an offlineDays value (default 30). Creating a product generates its first signing key, k1. You can rotate keys later, in two phases: the new key is first published as pending so you can ship it in an app update, and only signs once you activate it. In the dashboard: Products.

Plans#

A plan describes what a license grants (in the dashboard: Plans):

FieldMeaning
codeShort slug you choose, e.g. pro. It appears in certificates as plan.
nameDisplay name.
kindsubscription, perpetual or trial.
maxActivationsMachines per seat. Default 1.
entitlementsArray of strings, e.g. ["export", "ocr"], carried in the signed certificate.
maxMajorHighest major version of your app covered (integer), or null for all versions.
trialDaysLength of a trial, for kind: "trial".
offlineDaysOptional override of the product’s offlineDays for this plan (1 to 365), or null to use the product value.

License keys#

A license key looks like KR-7K2M9-Q4X1C-8D3F6-H0J5N: your product’s prefix (2 to 8 uppercase letters or digits, KR by default), then 20 random Crockford base32 characters in four groups of five. Crockford base32 skips I, L, O and U, so keys survive being read aloud or typed by hand. That is 100 bits of entropy.

Keys are matched case-insensitively and ignoring spaces: kr-7k2m9-q4x1c-8d3f6-h0j5n and KR-7K2M9-Q4X1C-8D3F6-H0J5N are the same key. Keyrail looks keys up by their SHA-256 hash and never writes them to logs.

License statuses#

StatusMeaningUsable
activeSubscription paidYes, until validUntil
past_dueA renewal payment failed, the provider is retryingYes, until validUntil
canceledSubscription canceled, runs to the end of the paid periodYes, until validUntil
expiredPast validUntilNo
perpetualOne-time purchaseYes, subject to maxMajor
suspendedSuspended by you (reversible), or subscription pausedNo
refundedFully refunded. A new payment lifts it.No
revokedChargeback or manual revocation. Final.No

expired is computed when the license is read: a license whose status is active, past_due or canceled and whose validUntil is in the past is reported as expired.

  • paidUntil is the end of the last paid period, set from your payment provider’s webhooks. It is null for perpetual licenses.
  • validUntil = paidUntil + the product’s graceDays (14 by default). It is null for perpetual licenses.

The grace period covers late renewals: a card that fails on the renewal date and succeeds two days later never interrupts your customer. If the payment never comes, the license becomes expired at validUntil.

Trials: a plan of kind trial creates a license with paidUntil = now + trialDays and status active.

Perpetual licenses and versions: with maxMajor: 1, a perpetual license works with every 1.x release of your app and is reported as version_not_covered by version 2.0. You can raise maxMajor per license (for an upgrade purchase) with PATCH /v1/licenses/{id}.

Activations and seats#

An activation is one machine using a license. The activation limit of a license is plan.maxActivations × license.seats, unless you override it on the license with maxActivations. Activating beyond the limit fails with 409 activation_limit.

Activating a machine that is already activated is not an error: it refreshes the activation. To move a license to another computer, the customer deactivates the old machine from your app (the SDK deactivate call), from the customer portal, or you free it from the license page of the dashboard (Free this machine, or DELETE /v1/activations/{id}).

Machines and fingerprints#

The SDK reads a stable machine identifier (Windows: HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid; macOS: IOPlatformUUID; Linux: /etc/machine-id, then /var/lib/dbus/machine-id) and sends only its hash:

text
fingerprint = lowercase hex( SHA-256( "keyrail:" + productId + ":" + rawMachineId ) )

rawMachineId is the value as read, trimmed of surrounding whitespace, with its case unchanged (macOS returns uppercase). On Windows, the SDKs read the 64-bit registry view, so 32-bit and 64-bit builds of your app get the same fingerprint.

The raw identifier never leaves the machine. Because the product ID is part of the hash, the same computer has a different fingerprint for every product: two vendors cannot correlate machines.

How changes reach installed apps#

Your app trusts its certificate until the certificate’s expiresAt. A certificate is issued at activation and at every refresh, and expires at the earlier of validUntil and offlineDays after issue. offlineDays is the plan’s value when the plan sets one, otherwise the product’s.

  • The SDK refreshes during check(): once refreshAfter (one day after issue) has passed, check() calls the client API before answering. If the network is down, rate limited or the service fails, it keeps using the cached certificate while it is valid and tries again at the next check().
  • Renewals extend validUntil. The SDK picks the new dates at its next refresh.
  • Suspensions, refunds, revocations and expiry make the next refresh fail with a 403 error (license_suspended, license_refunded, license_revoked, license_expired). The SDK then clears its cached certificate, and the app loses access at once. The same happens with 404 unknown_key or not_activated, for example after you or the customer freed the machine.
  • An app that stays offline never hears about these changes: it keeps the access it already had until its current certificate expires, at most offlineDays after its last refresh.

offlineDays is the trade-off between resilience and reaction time. 30 days suits most desktop apps; lower it for products where revocation speed matters more than offline use. A plan can override it: for example 7 days for subscriptions (a cancellation reaches the app within a week) and 60 days for a lifetime plan whose buyers work offline for long stretches.

Your Keyrail plan limits#

Plan limits (active licenses, products) are soft: going over never blocks your customers’ licenses. The dashboard shows a warning and the owner receives an email. Only creating a product beyond the limit is refused, with 402 plan_limit. See pricing.

View as Markdown

Need help? See Troubleshooting or write to support@keyrail.tech.