How every SDK behaves

The six KeyGrant SDKs follow one contract, so these rules hold in every language. Each SDK's page has the names, code and error lists in that language, and links here for the rules.

Calls are named here as the Electron SDK names them. The calls in each language gives each one's name in the other SDKs, and Which version has what, last, the version of each SDK that has each feature.

States and reasons

A status has one of six states and, on expired, invalid and unknown, one of ten reasons. They are written here as the server words them; each SDK page gives them in its own names.

statereasonMeans
licensedA valid lease for this key, this device and this build, or a stored offline license file that licenses this build.
trialA valid trial lease.
expiredofflineThe lease has run out and has not been renewed, because KeyGrant did not respond, returned a response that neither renews nor refuses the key (a server error or a rate limit, for example), or has not been contacted again yet.
expiredtrialThe trial has ended, or its lease no longer verifies here.
invalidrevokedKeyGrant refused the key because it was revoked, it is past its expiry and grace period, or this device's seat was released.
invalidupgradeThis build is a newer major version than the key covers: sell an upgrade.
invalidoutdatedThis build is an older major version than the key covers: update the app.
invaliddeviceThe license was activated on another machine.
invalidtamperedThe lease does not verify, or is for another product, key or activation.
invalidunknown-keyThe lease was signed by a key this build does not carry, such as one the product switched to after this build was made. See Rotating the signing key.
invalidrevoked-fileThe stored offline license file is one this build lists as revoked. See Blocking a leaked file.
unlicensedNo key and no trial is stored.
unknownstorageThe stored license could not be read. Keep running as before and check again shortly. Do not show the activation screen for this status.

What to tell the customer for each reason is in Offline validation & grace.

Activation errors

What an activation answers when it does not activate, as the server words it. A key locked to its buyer can also answer the seven errors of Keys only their buyer can activate.

  • Activating the same key again on the same device keeps its seat.
  • Activating a different key replaces the stored license without releasing the old key's seat: deactivate first to free it.
invalid-key

The key is empty or longer than 120 characters (no request is made), or KeyGrant does not recognize it.

limit

The key is already active on as many devices as its device limit allows, or more after its limit was lowered.

revoked

The key is revoked, or past its expiry and grace period.

upgrade

This build is a newer major version than the key covers.

outdated

This build is an older major version than the key covers.

invalid-lease

KeyGrant returned a lease that does not verify on this device, for example one signed by a key this build does not carry. KeyGrant has assigned the seat, but nothing is stored on the device.

network

KeyGrant did not respond (offline, or a timeout), or returned any other response.

  • Any other response: a server error, a rate limit, a product ID it does not know, an archived product (on a device that holds no seat on the key), or a request it rejected as malformed.
  • An activation also fails with it, without a request, while an earlier call is stuck, or when the device cannot be identified, such as when this device activates the same key again and cannot read its machine ID in time.

Keys only their buyer can activate

When a key is locked to its buyer (Who can activate), a new device must give the email the key was sold to and, for the code form, the code KeyGrant emails to it. The app asks for each only when an error names it:

  1. Activate the key as usual.
  2. On user-email-required, ask for the email the key was sold to, and activate again with it.
  3. On user-code-required, KeyGrant has just emailed a code to that address. Ask for it, and activate again with the email and the code.
  4. On try-again, the code could not be emailed and nothing was activated. Offer to try again.
  • Only a new device is asked. status(), refresh(), a renewal and a device that already holds a seat on the key never are.
  • Nothing is stored. Neither the email nor the code is kept on the device.
  • The address is trimmed and compared without regard to case. The code has its spaces dropped (123 456 is 123456) and must be exactly six digits.
  • A key that is not locked ignores both once they reach KeyGrant. A code that is not six digits (user-code-invalid), or an address over 254 characters (user-email-mismatch), is refused before any request, for any key.
  • A key file of a locked key takes the same email and code when it is imported. Deactivating, refreshing and starting a trial take neither.

Each SDK page lists the seven errors in its own names. How long a code lasts and how many are sent is in the rules of a lock.

Restoring a license without the key

Apple does not let an App Store build unlock itself with a typed license key. Key pickup restores a customer's license by the email they bought with instead, and the key is never shown. Turn on Key pickup on the product's Settings tab first.

  1. Ask for the email the customer bought with, and call pickUp(email). KeyGrant emails a six-digit code there, and answers the same whether or not the address has a key.
  2. Ask for the code that came, and call restoreByEmail(email, code). It confirms the code and activates the key that fits this build.

Of the address's live keys of the product that cover this build, a restore activates:

  1. the one this device already holds a seat on, so a reinstall takes its seat back;
  2. else the first with a free seat;
  3. else the first, whose activation answers limit.
  • No key at all: no-key.
  • Keys, none for this build: upgrade when they are for an older major version (the customer buys an upgrade), else outdated (the customer updates the app).
  • Every activation rule still holds: the key meets the device limit, its versions and its lock as a typed key does.

Let the customer choose

A sale of several keys gives every key the buyer's email. To let the customer pick one, call pickUp(email, code) instead of the restore, show each key by its modelName, and activate the one chosen with its key, the email and its activationCode.

  • A key locked to its buyer activates with that email, and one locked with a code takes the activationCode the pickup proved, so nothing more is asked.
  • Ask the customer for nothing but the address and the code, and never show a key.

What is kept

Nothing about a pickup is stored: not the address, the code or the keys. The keys a confirm answered are kept in memory for 10 minutes, so a restore with the same email and code needs no new code, after pickUp(email, code) or after an activation that got no answer.

Note

Your App Store build must still sell its unlock as an in-app purchase, which KeyGrant does not handle.

The requests and their errors are on POST /pickup, and their limits on Rate limits.

Pickup errors

What a pickup answers when it does not do what it asked, as the server words it:

invalid-email

No email, one longer than any email (no request is made), or one KeyGrant could not read.

wrong-code

Not the code KeyGrant emailed, or not six digits (no request is made, and it costs no try). Ask again.

code-expired

The address has no live code: it expired (it lasts 10 minutes), was used, was voided by its fifth wrong try, or was never asked for. Call pickUp(email) for a new one.

too-many-attempts

Thirty wrong codes for the address this hour, or too many requests from this network. retryAfter says how many seconds to wait, when KeyGrant gives it.

pickup-disabled

The product's Key pickup is off.

try-again

KeyGrant could not take the request just now. Try again.

network

KeyGrant did not respond, or returned something that is not an answer.

no-key

From restoreByEmail(): the address has no live key of the product. Its other errors are upgrade and outdated (the address has keys, none for this build) and the activation errors.

License files

A license can come as a file, .kglicense, that the customer drops onto the app or opens from its license screen. What each kind means, and what it costs, is under Delivered as in License models.

  • A key file carries the customer's key. Importing it activates the key online, exactly as typing it would, and the file itself is not stored.
  • An offline license file licenses the app with no network at all, for good. Importing it stores it, in place of any file stored before.

These are not the file the SDK stores its own state in (Where the license is stored).

Warning

Keep every key that signed a file in your builds.

A file names the key that signed it and never expires, so a key that has signed one stays in the product's key set, and in your build's public keys, after it stops signing leases. A build without it reads such a file as invalid with reason unknown-key.

Importing a file

The SDK takes the file's content, as text or bytes, never its path: your app reads the file and hands it over. A file is at most 16,384 bytes, and its media type is application/vnd.keygrant.license.

  • It is checked offline: signed by a key of the build's key set, for this product, and, for an offline file, covering this build's major version.
  • A refused file stores nothing. Each SDK page lists the eight refusals, from malformed to outdated.
  • A key file whose key did not activate answers what an activation would.
  • A key file of a locked key takes the same email and code as an activation; an offline file ignores them.
  • It fails (throws or rejects) while an earlier call has not finished, when the file cannot be saved, and, for a key file, wherever an activation fails.

Blocking a leaked file

An offline file cannot be revoked by KeyGrant. To stop one that leaked, list its id (fid) in the build's revoked license files and ship an update.

  • Ids are matched exactly. The list applies to both kinds; for a key file, revoking the key is the real control.
  • A stored file on the list reads as invalid with reason revoked-file, and importing one is refused with revoked-file.
  • Anything but a list of texts is a configuration error, refused when the license object is made.

The status of a stored file

An offline file is checked again on every status(), which then answers licensed with its source (a file), the key, who it is licensed to and the file's entitlements. It never asks KeyGrant, refresh() included.

  • Show who it is licensed to, as in "Licensed to Ada Lovelace".
  • A file holds no seat. Offer to remove the file instead of deactivating; removing it leaves a key the device holds working, and deactivate() leaves the file alone.
  • A file and a key on one device: a file that covers this build wins, and the key keeps working once the file is removed, revoked or outgrown.
  • A file that does not cover this build answers only when no key and no running trial does.

A status from a file can be invalid with one of these reasons:

ReasonMeans
revoked-fileThe build lists the file's id as revoked.
upgradeThis build is newer than the file covers.
outdatedThis build is older than the file covers.
tamperedThe stored file is not a license file: the state file was edited.
unknown-keyThis build does not carry the key that signed it. No renewal comes for a file, so update the app, or ask for the same license's file signed again.

Metered uses

A license model can count uses of what it sells: 1,000 exports for good, or 50 renders each billing period (Metered uses). The app spends them with use(meter, n) before the work they count, and does the work only when the spend is ok.

  • Online it is exact. KeyGrant takes n only while the meter has room for all of it.
  • Two calls are two spends. The SDK's own retries and reports never count one twice, but an app that calls twice has spent twice.
  • It needs a key's lease that licenses this build on this device (not-licensed otherwise, without asking). A device trial and an offline license file have no meters.
  • A meter never takes the license away. Nothing reads one to decide whether the app is licensed: a meter that is used up refuses spends, never the app.
  • No outcome is an exception. Every outcome is in the call's answer. A request waits at most 5 seconds for KeyGrant, less when the SDK's request timeout is shorter.
  • What is asked: n is a whole number from 1 to 1,000,000,000 (else invalid-amount), and a meter code is 1 to 32 UTF-16 units, matched exactly, case included (else no-meter). Neither asks KeyGrant anything.
  • An older SDK: an app built with an SDK from before metered uses licenses a metered key as any other and cannot spend.

With no answer

No network, a timeout, a server error, a rate limit or an answer the SDK cannot use: the device spends from its offline allowance for the meter, and the answer says it was spent offline.

  • The allowance is set on the license model: a tenth of the meter by default, 0 for online only.
  • The limit: never past the allowance, and never past what the device last heard is left. Past either, the answer is offline-limit.
  • Reporting: the SDK reports the spend with its next request to KeyGrant.
  • Back online: a spend the allowance would refuse asks KeyGrant first, whatever the last outage left behind, so a customer who is back online is not refused for it.

Unsynced spends

Spends KeyGrant has not heard of are kept with the license, so they survive a restart and a second instance of the app. deactivate() sends them with the release and clears them locally even when KeyGrant could not be reached. What KeyGrant did not hear is then lost: at most the offline allowance of each meter.

What is left

The status has the key's meters, by meter code, on a licensed status from a key's lease that names meters. It is answered offline, and has no meters on any other status. Each meter has:

left

What is left: the uses the lease said were left, less what this device has spent since and not reported. Never below 0.

total

The uses the key holds, for good or for this billing period, with any added on top.

offline

How many uses this device may spend while KeyGrant cannot be reached.

unsynced

Uses this device spent that KeyGrant has not heard of yet.

resets

true for a meter that comes back to its full uses each billing period.

Where the license is stored

The default storage is one JSON file per product, <product>.json, in a per-user folder on this machine. Every SDK writes the same format to the same folder, so each can read the stored license another SDK wrote.

  • Windows: %LOCALAPPDATA%\KeyGrant, or <home>\AppData\Local\KeyGrant if that variable is not set. The roaming %APPDATA% folder is never used.
  • macOS: ~/Library/Application Support/KeyGrant.
  • Linux and others: $XDG_CONFIG_HOME/keygrant, or ~/.config/keygrant if that variable is not set.
  • iOS and Android (the Dart SDK): KeyGrant/<product>.json in the app's support folder.

An empty or relative environment variable counts as unset. How a save works:

  • Atomic: a temporary file, then a rename.
  • Merged: a save reads the file again just before it replaces it, and merges onto a save another process made in the meantime, in up to 5 attempts.
  • A file that cannot be parsed is renamed to <product>.json.<time>.corrupt (the newest 3 are kept), and the device is unlicensed until it activates again.

Each SDK can take its own storage instead; its page says how, and where it differs (a sandboxed Mac app, a phone).

Which version has what

An SDK older than the version below does not have the feature.

FeatureElectron.NET, Rust, Swift, C++Dart
License files0.6.00.5.00.1.0
Keys only their buyer can activate0.7.00.6.00.1.0
Metered uses0.8.00.7.00.1.0
Key pickup0.9.00.8.00.1.0

Dart 0.1.0 is the Dart SDK's first version.