CLI
The keygrant command works with KeyGrant licenses from a terminal or a build pipeline. It is
the @keygrant/cli package, version 0.5.3.
npm install --global @keygrant/cli
# Paste a secret API key when it askskeygrant login
# Your products and their license modelskeygrant products Install and run
Run it with npx, or install it globally. It needs Node.js 22 or later.
npx @keygrant/cli --help# ornpm install --global @keygrant/clikeygrant --help keygrant --helplists the commands, andkeygrant <command> --helpgives every option of one.keygrant --versionprints the version.-
The
licensesin a command may also be spelledlicense,licencesorlicence.
One file, nothing to install
From version 0.5.0 each release also has the command as one file for each platform, with nothing to install,
not even Node.js. Each file has a .sha256 beside it, on the release page of
keygrantdev/keygrant-cli.
- Linux: x64 and arm64, glibc 2.28 or later, so not Alpine.
- macOS: Apple silicon and Intel.
- Windows: x64.
It keeps no key: set KEYGRANT_API_KEY, as a build pipeline does.
From 0.5.1 the macOS files are signed with a Developer ID and notarized by Apple, and from 0.5.2 the
Windows file is signed too. Each release after 0.5.0 says in its notes whether its macOS files are
notarized and its Windows file signed. Version 0.5.0's are neither: macOS may stop the file the first time
(xattr -d com.apple.quarantine <file> lets it run), and Windows may ask before running it.
Signing in
The command uses one of your account's secret API keys (kg_live_... or
kg_test_...), from the dashboard's API keys screen, never your dashboard
sign-in.
Run keygrant login. It asks for the key without echoing it (or reads it from standard input), checks it with KeyGrant, and keeps it in the operating system's credential store, for the KeyGrant address it checked it with.
- The stores: the macOS Keychain, the Windows Credential Manager, or the Secret Service (GNOME Keyring, KWallet) on Linux.
- On Linux the Secret Service is required: a machine without one (no desktop session, WSL) has no store that outlives a restart, so the key is not kept there.
keygrant logoutremoves the kept key. It does not revoke the key: do that on the API keys screen to stop it working.
Set KEYGRANT_API_KEY from the pipeline's secrets. Where there is no credential store, keygrant login says so and keeps nothing.
KEYGRANT_API_KEY, when set, is used before a kept key.-
A
kg_test_key issues and reaches test licenses only; a release needs akg_live_key. See Server endpoints for what each kind of key reaches. pubkeyneeds no API key, andverifyworks offline.
Commands
| Command | Does |
|---|---|
| Sign in | |
keygrant login | Keep a secret API key in the credential store |
keygrant logout | Remove the kept key |
| Products and releases | |
keygrant products | Your products, with their license models and each model's id |
keygrant release <version> --product <id> | Tell KeyGrant you shipped a version |
keygrant pubkey <product id> | The product's public key set, the JSON your app embeds |
| Issue and find licenses | |
keygrant licenses issue --product <id> --model <model id> | Issue a key (--count for several, as one sale) |
keygrant licenses get <key> | One license |
keygrant licenses list [--product] [--email] [--status] [--all] | Licenses, newest first |
keygrant licenses checkout <cs_... or txn_...> | The license a Stripe checkout or a Paddle transaction made |
| Change a license | |
keygrant licenses extend <key> --to <date> or --days <n> | Move its expiry later |
keygrant licenses versions <key> --min <n> --max <n|none> | Set the major versions it covers |
keygrant licenses device-limit <key> <n> | Set how many devices it may be active on |
keygrant licenses seat-release <key> <days|off> | Set how many days an unused device keeps its seat before it is freed, or off |
keygrant licenses user-lock <key> <off|email|code> | Set who can activate a new device on it: anyone with the key, or only the buyer |
keygrant licenses revoke <key> [--reason <reason>] | Revoke it |
| Devices | |
keygrant licenses devices <key> | The devices it is active on, with each one's activation id |
keygrant licenses free-device <key> <activation id> | Free one device |
| Testing | |
keygrant licenses test --product <id> --scenario <state> | A test license: valid, trial-ending, expired, revoked, device-limit or used-up |
| Meters | |
keygrant licenses meters <key> | What the license counts: each meter's uses left, total, used, offline allowance and period |
keygrant licenses uses <key> <meter> --add <n> (or --set <n>, --offline <n|default>, --resets <never|period>, --reset, --remove) | Add uses to a meter, set its uses, its offline allowance or whether it comes back each period, start a new period, or take it off the key |
keygrant licenses use <key> <meter> [n] [--id <id>] | Spend a meter from your backend |
| Files and verify | |
keygrant licenses file <key> [--out <path>] [--force] [--resign] | Save the key's license file or key file (a .kglicense), for a license model that delivers a file |
keygrant verify <lease | license file> --key <file> | Verify a lease, a license file or a key file against a key set, with no network |
Examples
# A release scriptkeygrant release "$VERSION" --product my-app
# The key set to build into the appkeygrant pubkey my-app > src/keygrant-keys.json
# A test license whose device limit is already reached, to see how the app handles itkeygrant licenses test --product my-app --scenario device-limit
# Check a lease the app stored, offlinekeygrant verify ./lease.txt --key src/keygrant-keys.json --product my-app
# Save the license file of a key sold as a file, to send it yourselfkeygrant licenses file MYAPP-AAAA-BBBB-CCCC-DDDD --out ./files/ada.kglicense
# Check a license file or key file a buyer was sent, offline: its kind, product, licensee, versions,# file id and the key that signed it (the key set must hold every key that ever signed a file)keygrant verify ./my-app-license.kglicense --key src/keygrant-keys.json --product my-app
# Give a customer 500 more exports, once, however often the command runskeygrant licenses uses MYAPP-AAAA-BBBB-CCCC-DDDD exports --add 500 --idempotency-key support-ticket-4812
# Spend 3 renders for a customer from your own server, once per renderkeygrant licenses use MYAPP-AAAA-BBBB-CCCC-DDDD renders 3 --id render-9f2c1a77-0001
# Let only the buyer activate a new device on a key: their email, then a code emailed to itkeygrant licenses user-lock MYAPP-AAAA-BBBB-CCCC-DDDD code Products, releases and keys
Look up your products, report a release and fetch a product's public keys.
keygrant products
keygrant products [--product <id>] prints your products, with the meters their apps spend
and their license models' ids, which --model takes.
- Each product shows its API ID, key prefix and current version, and "key pickup on" when its apps may restore a license by an emailed code (from 0.5.3).
- A model whose keys are sent as a file says so after what it sells (as a key file, as a license file).
-
A model that counts uses shows its meters: the code, its uses, "a period" when they come back each billing
period, and its own offline allowance. A top-up shows the uses a unit adds (
+500 exports). - When a model locks who can activate its keys, a column says who can, for each model.
One product, by its API ID.
Prints the API's answer, { "products": [ ... ] }, with only that product in it when --product is given.
keygrant release
keygrant release <version> --product <id> moves the product's current version up to
this version's major (2.4.0 is 2), never down.
- A build script may run it on every release, a hotfix on an old line included.
- The current version caps what upgrade protection keeps (see Current version).
- It needs a
kg_live_key, and refuses akg_test_one. -
It prints the current version after the call;
--jsonprints the API's answer,{ "product", "currentMajor" }.
See POST /v1/products/:slug/releases.
keygrant pubkey
keygrant pubkey <product id> [--signing-only] prints { "keys": [ ... ] }:
every key a build should carry, each with its id (kid). It is the set an app embeds (see
GET /pubkey).
- Embed the whole set, so a lease verifies on either side of a key switch.
-
--signing-onlyprints the key that signs, alone: what a build carries after a signing key leaked. - It needs no API key.
--jsonprints the API's answer,{ "publicJwk", "keys" }.
Licenses
Issue, find, change and revoke licenses, free their devices and make test licenses.
keygrant licenses issue
keygrant licenses issue --product <id> (--model <model id> | --type <type>)
issues a key, or several as one sale. It prints each key on a line of its own.
A kg_test_ key issues test licenses. See POST /v1/licenses.
Required. The product's API ID.
The license model sold, by its id (keygrant products lists them). Its terms are the key's: expiry, devices, entitlements and versions.
perpetual, subscription, trial or timed, for a key sold under no model. Give one of --model and --type, not both.
The customer's address. Required by a model that sends a file or locks who can activate (email-required), unless the key is a test license.
Email the key with the product's delivery mail.
The device limit, in place of the model's.
The days an unused device keeps its seat before it is freed, in place of the model's, and never fewer than its Offline days. off for never.
When it ends, in one of the forms of a date.
The version sold, "2.4.1". Only its major counts.
The majors it covers, in place of the model's. Give both or neither.
Months of upgrade protection, in place of the model's.
What it grants, in place of the model's. Repeat it for each one; the value is true, false, a number or text.
Grant none, whatever the model grants. It cannot be given with --entitlement.
What it counts, in place of the model's. Repeat it for each meter: the uses, then any of these, as in renders=50,period,offline=5.
period: back each billing period, on a subscription key.never: used up for good.offline=<n|default>: a device's offline allowance.
Count nothing, whatever the model counts. It cannot be given with --meter.
Keys to issue, as one sale.
The same key on a retry answers the key first issued. One is made for each run otherwise.
Prints the API's answer, { "key", "emailed" }, and "keys" with --count.
keygrant licenses get, list and checkout
keygrant licenses get <key> prints the license: its model, state, who can activate it, how
it was delivered, expiry, devices, versions and entitlements. --json prints the API's answer,
{ "license": { ... } }.
keygrant licenses list lists licenses, newest first, as a table, and prints the cursor of the next
page when there is one.
One product's licenses, by its API ID.
One customer's licenses, by their whole address.
all, active, trial, expired or revoked.
Licenses a page.
The page after the one that printed it.
Every page, one after another.
Prints the API's answer, { "licenses": [ ... ], "nextCursor": ... }; with --all, every license and a nextCursor of null.
keygrant licenses checkout <cs_... or txn_...> prints the license a Stripe Checkout Session
or a Paddle transaction minted (every key, for a sale of several), or why there is none:
- not yet: the webhook that makes it can take a few seconds;
- the checkout upgraded a key;
- the sale was held (see Held sales);
- the sale was refunded.
keygrant licenses extend, versions, device-limit and seat-release
Each prints the license as it now stands. --json prints the API's answer.
keygrant licenses extend <key> (--to <date> | --days <n>) moves the expiry later.
--tois the new expiry, in one of the forms of a date.--dayscounts from the later of now and the key's expiry.- It never shortens an expiry and never reinstates a revoked key.
- A timed key still waiting for its first activation has no end to move yet (
term-not-started), and a perpetual key none at all (not-expiring). - The new expiry is sent as an instant, so running the command again with
--tochanges nothing more.
keygrant licenses versions <key> --min <n> --max <n|none> [--upgrade-until <date|none>] sets the majors the key covers, the whole range every time.
--minis its lowest major.--maxis its highest, ornonefor "and every later one".--upgrade-untilis where its upgrade window ends (every major runs until then), ornone.
keygrant licenses device-limit <key> <n> sets the device limit to n, in place of its model's.
- Lowering it frees no device: a new device is refused until fewer than
nare active. See Change the limit.
keygrant licenses seat-release <key> <days|off> sets how many days a device of the license may go without checking in before the nightly pass frees its seat, in place of its model's.
offkeeps every seat.- It is never fewer than the key's Offline days (
invalid-seat-release). - A device already unseen that long is freed the next night. See Free unused seats.
keygrant licenses user-lock
keygrant licenses user-lock <key> <off|email|code> sets who can activate a new device
on the key, in place of its model's. It prints the license as it now stands; --json prints the
API's answer. From version 0.5.0.
Anyone with the key. Always allowed, whatever the key.
Only the buyer, who names the email the key was sold to.
Only the buyer, who also gives a code KeyGrant emails to it.
- Only a new device is ever asked: devices already activated keep working.
- Your app sends the email and the code when it activates.
-
These cannot be locked: a key with no email (
email-required), a test license (user-lock-test-key) and a key sent as an offline license file (user-lock-license-file). -
KeyGrant emails no code for a product that sends its own customer email, so a new device on a
codekey is toldtry-againand cannot activate: useemailthere, or let KeyGrant send the product's email. -
A key file carries the buyer's email, so where KeyGrant sends the product's email, use
codefor a key sent as one. keygrant licenses getsays when a key's lock is never asked, so anyone can activate it.
See Who can activate and POST /v1/licenses/:key/user-lock.
keygrant licenses revoke, devices and free-device
keygrant licenses revoke <key> [--reason <reason>] revokes the key. The app stops at its next check-in, or when its lease runs out offline.
--reasonisrefund,chargeback,subscription_canceledormanual.--jsonprints{ "ok": true }.
keygrant licenses devices <key> lists the devices the key is active on, the most recently seen first.
- For each: its activation id; its name, platform and app version as the app reported them (control characters shown as
?); when it last checked in and when it activated. FREED AFTER, when the key frees unused seats: the time after which the first nightly pass frees its seat unless it checks in first.--jsonprints the API's answer, with each device's fingerprint too. See GET /v1/licenses/:key/activations.
keygrant licenses free-device <key> <activation id> frees that seat so another device can take it.
- The activation id is the one
keygrant licenses deviceslists, the SDK's, and thedevice.activatedwebhook's. --jsonprints{ "released": 0 | 1 }.
keygrant licenses test
keygrant licenses test --product <id> [--scenario <state>] makes a test license in a
state your app must handle, as the dashboard's Create test license does. It prints the key on
its first line, then the license.
- It is signed with the product's real key and marked as a test, so it works in the build you ship.
- It lasts 30 days at most, is never emailed, and your account holds 25 at once. See Testing your integration.
Required. The product's API ID.
valid, trial-ending, expired, revoked, device-limit or used-up.
device-limitfills its one seat, so your machine is refused.used-upleaves its first meter no uses, so a spend is refused.
Its license model, else the product's first that issues keys.
trial-endingalways takes the product's trial model, so it refuses--model.used-uptakes the first of those that counts uses; a model that counts none isno-meters.
The version it is for, else its first activation's.
The same key on a retry answers the license first made.
Prints the API's answer, { "key", "scenario", "license" }.
Meters
A license model can count uses of what it sells: 1,000 exports for good, or 50 renders each month a
subscription is paid (see Metered uses). Each thing counted is a
meter, with a code your app passes to use(). Your app spends its own meters
through the SDK; the command line is for you.
keygrant licenses meters
keygrant licenses meters <key> prints each meter of a license:
- left: what is left;
- total: the uses it holds, added ones included;
- used, and what was added on top;
- offline: the allowance a device may spend while KeyGrant cannot be reached
(
(default)when the key sets none of its own); - resets:
never, or each billing period and where this one ends; - overage, when any meter has some: spent past the total, which only reports from offline can make.
keygrant licenses get shows the same in a line, and keygrant products lists a
product's meters. --json prints the API's answer.
keygrant licenses uses
keygrant licenses uses <key> <meter> changes one meter, with exactly one of these:
Adds n uses on top of the key's own: a top-up by hand.
Sets the key's own uses to n. What was added stays. A meter the key lacks but its product has is added with them.
The most uses a device may spend while KeyGrant cannot be reached: n, or default (a tenth of the meter's total). On a meter the key holds.
Whether the meter is used up for good (never) or comes back each billing period (period, on a subscription key only). On a meter the key holds.
Starts a new period now, on a meter that comes back each billing period.
Takes the meter off the key, discarding the uses added on top and the count of what was spent; adding it back starts it unused.
- Offline spends a device reports while the meter is off the key are not counted, and the app's
use()of it is then refused asno-meter.
For --add and --reset: the same key on a retry changes nothing twice. One is made for each run otherwise. The other changes refuse it.
-
--addand--resetsend an idempotency key: see Retries of your own. -
A set is its own retry: run again, it changes nothing more. A removal run again answers
unknown-meter, the meter being gone already. - The app sees a change at its next check-in.
- It prints what was done and the license as it now stands;
--jsonprints the API's answer.
See Meters: /v1/licenses/:key/meters.
keygrant licenses use
keygrant licenses use <key> <meter> [n] [--id <id>] spends n uses
from your own backend, such as a cloud render the key pays for. Your secret key is the proof, so no device is
asked.
The uses to spend.
The spend's id, one made for each run otherwise. The same id again answers as the first spend did and spends nothing more.
Prints the API's answer, { "meter": { ... } }, without the spend's id.
-
It is exact and whole or none: when fewer than
nare left it exits 1 withused-upand how many are. -
It is refused for a revoked key (
license-revoked) and an expired one (license-expired). - An error that may have spent all the same (exit 5) names the id it made.
- It prints what it spent, what is left and the spend's id.
License files and key files
A license model can deliver a key as a file (.kglicense) instead of typed text. The buyer is
emailed the file when they buy.
- A key file carries the key: an app that takes it activates the key online as it would a typed one.
- A license file licenses the app offline, for good, on any number of computers.
What each kind means is under Delivered as in License models.
keygrant licenses file
keygrant licenses file <key> saves the key's file: as it was kept while the key says the
same, otherwise signed again with the current signing key. It prints where it went.
Where to save it. By default it is saved under its own name, <product>-license.kglicense, in the current folder.
Replace a file that is already there. Without it the command refuses.
Sign it again with the key that signs now, keeping its id while it grants the same: the remedy after a signing key leaked. The file must then be sent to the buyer again.
Prints the answer with the file's content left out, and the path it was saved to.
- Each call is in the license's history.
- It writes the file readable by you alone on macOS and Linux.
-
KeyGrant refuses a key delivered typed (
not-a-file-license), a key with no email address (no-licensee), a revoked key (license-revoked) and an offline file's key that ends (license-expires).
See POST /v1/licenses/:key/file.
keygrant verify
keygrant verify <lease | license file | - | file> --key <file> checks a lease, a
license file or a key file against a key set, exactly as an SDK does, with no network. It exits 0 for a valid
lease or file and 1 for one that is not.
- A lease: give the lease itself,
-for standard input, or a file holding it (the token, or JSON with alease). - A license file or key file: give its path,
-for its text on standard input, or its token alone. A file whose name ends in.kglicense, or a token whose header says it is a license file's, is read as one.
Required. The key set keygrant pubkey prints, the pubkey route's answer, or one JWK.
Also require it to be this product's.
Judge a lease's expiry at this time instead of now: a date (its first moment, UTC), an ISO time with its offset, or milliseconds. A license file never expires, so --at does not apply to one.
Prints { "valid", "reason"?, "claims"? }, and for a file the kid its header names.
A lease is checked for, in this order:
- its signature, by the key it names (each key in turn for a lease that names none);
- its claims;
- its expiry.
It prints what the lease grants, and a metered key's meters with what was left of each when the lease was signed.
A license file is read as an app reads it:
- the text must be valid UTF-8 with one token (notes on lines starting with
#are ignored); - the key id it names must be in the key set;
- its signature must be by that key;
- its claims must be a license file's.
It prints the file's kind, product, licensee, key, versions (an offline file), file id and the id of the key
that signed it. The key set from keygrant pubkey holds every key that ever signed a file, so a
file signed before a key rotation still verifies.
Dates
A date option takes any of these forms.
| Form | Means |
|---|---|
2027-01-31 | A date: through that whole day, in UTC. For verify --at, its first moment. |
2027-01-31T12:00:00Z | A date and time in ISO 8601, with its offset. |
1801396800000 | Milliseconds since the epoch: 13 digits. Ten, the seconds habit, are refused. |
extend --days counts from the later of now and the license's current expiry. A timed license
still waiting for its first activation has no expiry to move yet.
For scripts
Every command takes --json and prints what the API answered.
-
A command's
--helpsays when it prints an object of its own:login,logout,verify,licenses file, a filteredproducts, and every page oflicenses list --all. -
When something fails,
--jsonprints the error on standard error, as{ "error": "<code>", ... }with"status"when KeyGrant answered, and nothing else there. -
Without it, an API error prints as
keygrant: <code>: <sentence>, and a usage error askeygrant: <sentence>. When a command was named, a line follows saying to run that command's--help.
| Exit code | Means |
|---|---|
| 0 | Done |
| 1 | KeyGrant refused the request (the code says why), a lease or license file is not valid, or something else failed |
| 2 | The command line is wrong, a file it names cannot be read or written, or licenses file would replace a file without --force |
| 3 | No API key, a key KeyGrant does not accept, or no credential store for login |
| 4 | Rate limited |
| 5 | KeyGrant could not be reached, or failed |
When KeyGrant answers 429
-
The command waits as long as the answer says, says so on standard error (not under
--json), and tries once more. A second429exits 4 with the wait. - A
429asking for more than two minutes is not waited out: it exits 4 at once. --no-retryexits 4 at once instead.- KeyGrant counts a request before it does anything, so the retry never does a thing twice.
The limits themselves are in Rate limits.
Retries of your own
-
licenses issue,licenses test, andlicenses useswith--addor--reset, send an idempotency key: one made for each run, or yours with--idempotency-key. Running the same command again with the same key makes or changes nothing more: an issue or a test answers the license first made. -
When one of them fails in a way that may have gone through all the same (exit 5: KeyGrant could not be
reached, or failed), the error names the key it made (
idempotencyKeyunder--json). Run the command again with--idempotency-keyand that key. -
licenses extendsends the new expiry as an instant, so running it again with--tochanges nothing more. -
licenses usesandlicenses useretry safely too: see keygrant licenses uses and keygrant licenses use.
Options every command takes
Print the answer as JSON (see For scripts).
KeyGrant's address; KEYGRANT_API_URL too. https:// only, or http:// for localhost, 127.0.0.1 or [::1].
Do not wait out a 429: exit 4 at once.
The command's help.