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.
| state | reason | Means |
|---|---|---|
licensed | A valid lease for this key, this device and this build, or a stored offline license file that licenses this build. | |
trial | A valid trial lease. | |
expired | offline | The 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. |
expired | trial | The trial has ended, or its lease no longer verifies here. |
invalid | revoked | KeyGrant refused the key because it was revoked, it is past its expiry and grace period, or this device's seat was released. |
invalid | upgrade | This build is a newer major version than the key covers: sell an upgrade. |
invalid | outdated | This build is an older major version than the key covers: update the app. |
invalid | device | The license was activated on another machine. |
invalid | tampered | The lease does not verify, or is for another product, key or activation. |
invalid | unknown-key | The 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. |
invalid | revoked-file | The stored offline license file is one this build lists as revoked. See Blocking a leaked file. |
unlicensed | No key and no trial is stored. | |
unknown | storage | The 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.
The key is empty or longer than 120 characters (no request is made), or KeyGrant does not recognize it.
The key is already active on as many devices as its device limit allows, or more after its limit was lowered.
The key is revoked, or past its expiry and grace period.
This build is a newer major version than the key covers.
This build is an older major version than the key covers.
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.
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:
- Activate the key as usual.
- On
user-email-required, ask for the email the key was sold to, and activate again with it. - 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. - 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 456is123456) 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.
- 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. - 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:
- the one this device already holds a seat on, so a reinstall takes its seat back;
- else the first with a free seat;
- else the first, whose activation answers
limit.
- No key at all:
no-key. - Keys, none for this build:
upgradewhen they are for an older major version (the customer buys an upgrade), elseoutdated(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
activationCodethe 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:
No email, one longer than any email (no request is made), or one KeyGrant could not read.
Not the code KeyGrant emailed, or not six digits (no request is made, and it costs no try). Ask again.
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.
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.
The product's Key pickup is off.
KeyGrant could not take the request just now. Try again.
KeyGrant did not respond, or returned something that is not an answer.
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
malformedtooutdated. - 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
invalidwith reasonrevoked-file, and importing one is refused withrevoked-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:
| Reason | Means |
|---|---|
revoked-file | The build lists the file's id as revoked. |
upgrade | This build is newer than the file covers. |
outdated | This build is older than the file covers. |
tampered | The stored file is not a license file: the state file was edited. |
unknown-key | This 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
nonly 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-licensedotherwise, 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:
nis a whole number from 1 to 1,000,000,000 (elseinvalid-amount), and a meter code is 1 to 32 UTF-16 units, matched exactly, case included (elseno-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:
What is left: the uses the lease said were left, less what this device has spent since and not reported. Never below 0.
The uses the key holds, for good or for this billing period, with any added on top.
How many uses this device may spend while KeyGrant cannot be reached.
Uses this device spent that KeyGrant has not heard of yet.
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\KeyGrantif 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/keygrantif that variable is not set. - iOS and Android (the Dart SDK):
KeyGrant/<product>.jsonin 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.(the newest 3 are kept), and the device is unlicensed until it activates again.<time>.corrupt
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.
| Feature | Electron | .NET, Rust, Swift, C++ | Dart |
|---|---|---|---|
| License files | 0.6.0 | 0.5.0 | 0.1.0 |
| Keys only their buyer can activate | 0.7.0 | 0.6.0 | 0.1.0 |
| Metered uses | 0.8.0 | 0.7.0 | 0.1.0 |
| Key pickup | 0.9.0 | 0.8.0 | 0.1.0 |
Dart 0.1.0 is the Dart SDK's first version.