# Concepts

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

## Overview

| Object | What it is | ID prefix |
|---|---|---|
| Account | You, the software vendor. Has a Keyrail plan. | `acc_` |
| User | A person who signs in to your dashboard. Roles `owner`, `admin`. | `usr_` |
| Product | A piece of software you sell. Owns its Ed25519 signing keys. Its ID is public and embedded in your app. | `prd_` |
| Plan | A license template inside a product: kind, activation limit, entitlements, maximum major version. | `pln_` |
| Customer | One of your end users (email, name). | `cus_` |
| License | What a customer bought: a license key, a status and validity dates. | `lic_` |
| Activation | One machine using a license. | `act_` |
| Integration | A connection to Paddle Billing or Lemon Squeezy. | `int_` |
| API key | A 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](/docs/offline-certificates/#key-rotation) 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](/docs/dashboard/#products).

## Plans

A plan describes what a license grants (in the dashboard: [Plans](/docs/dashboard/#plans)):

| Field | Meaning |
|---|---|
| `code` | Short slug you choose, e.g. `pro`. It appears in certificates as `plan`. |
| `name` | Display name. |
| `kind` | `subscription`, `perpetual` or `trial`. |
| `maxActivations` | Machines per seat. Default 1. |
| `entitlements` | Array of strings, e.g. `["export", "ocr"]`, carried in the signed certificate. |
| `maxMajor` | Highest major version of your app covered (integer), or `null` for all versions. |
| `trialDays` | Length of a trial, for `kind: "trial"`. |
| `offlineDays` | Optional 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

| Status | Meaning | Usable |
|---|---|---|
| `active` | Subscription paid | Yes, until `validUntil` |
| `past_due` | A renewal payment failed, the provider is retrying | Yes, until `validUntil` |
| `canceled` | Subscription canceled, runs to the end of the paid period | Yes, until `validUntil` |
| `expired` | Past `validUntil` | No |
| `perpetual` | One-time purchase | Yes, subject to `maxMajor` |
| `suspended` | Suspended by you (reversible), or subscription paused | No |
| `refunded` | Fully refunded. A new payment lifts it. | No |
| `revoked` | Chargeback 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`.

## Paid period and grace period

- `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](/docs/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](/pricing/).
