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.

Quick start
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.

Terminal
npx @keygrant/cli --help# ornpm install --global @keygrant/clikeygrant --help
  • keygrant --help lists the commands, and keygrant <command> --help gives every option of one.
  • keygrant --version prints the version.
  • The licenses in a command may also be spelled license, licences or licence.

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.

On your own machine

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 logout removes the kept key. It does not revoke the key: do that on the API keys screen to stop it working.
In a build pipeline or a container

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 a kg_live_ key. See Server endpoints for what each kind of key reaches.
  • pubkey needs no API key, and verify works offline.

Commands

CommandDoes
Sign in
keygrant loginKeep a secret API key in the credential store
keygrant logoutRemove the kept key
Products and releases
keygrant productsYour 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

Terminal
# 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.
--product <id>

One product, by its API ID.

--json

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 a kg_test_ one.
  • It prints the current version after the call; --json prints 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-only prints the key that signs, alone: what a build carries after a signing key leaked.
  • It needs no API key.
  • --json prints 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.

--product <id>

Required. The product's API ID.

--model <model id>

The license model sold, by its id (keygrant products lists them). Its terms are the key's: expiry, devices, entitlements and versions.

--type <type>

perpetual, subscription, trial or timed, for a key sold under no model. Give one of --model and --type, not both.

--email <address>

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.

--send-email

Email the key with the product's delivery mail.

--device-limit <n>

The device limit, in place of the model's.

Range 1 to 1,000

--seat-release-days <n|off>

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.

Range 1 to 365 days

--expires <date>

When it ends, in one of the forms of a date.

--version <version>

The version sold, "2.4.1". Only its major counts.

--min-major <n>, --max-major <n|none>

The majors it covers, in place of the model's. Give both or neither.

--upgrade-months <n>

Months of upgrade protection, in place of the model's.

Range 0 to 120 months

--entitlement <code=value>

What it grants, in place of the model's. Repeat it for each one; the value is true, false, a number or text.

--no-entitlements

Grant none, whatever the model grants. It cannot be given with --entitlement.

--meter <code=uses,...>

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.

Uses 0 to 1,000,000,000 ; Since 0.5.0

--no-meters

Count nothing, whatever the model counts. It cannot be given with --meter.

Since 0.5.0

--count <n>

Keys to issue, as one sale.

Range 1 to 100

--idempotency-key <key>

The same key on a retry answers the key first issued. One is made for each run otherwise.

Length 1 to 255 visible ASCII characters, no space at either end

--json

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.

--product <id>

One product's licenses, by its API ID.

--email <address>

One customer's licenses, by their whole address.

--status <state>

all, active, trial, expired or revoked.

Default all

--limit <n>

Licenses a page.

Range 1 to 100 ; Default 25

--cursor <c>

The page after the one that printed it.

--all

Every page, one after another.

--json

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

keygrant licenses extend <key> (--to <date> | --days <n>) moves the expiry later.

  • --to is the new expiry, in one of the forms of a date.
  • --days counts 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 --to changes nothing more.

--days 1 to 36,500

keygrant licenses versions

keygrant licenses versions <key> --min <n> --max <n|none> [--upgrade-until <date|none>] sets the majors the key covers, the whole range every time.

  • --min is its lowest major.
  • --max is its highest, or none for "and every later one".
  • --upgrade-until is where its upgrade window ends (every major runs until then), or none.

--min 1 to 99,999

keygrant licenses device-limit

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 n are active. See Change the limit.

Range 1 to 1,000

keygrant licenses seat-release

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.

  • off keeps 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.

Range 1 to 365 days

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.

off

Anyone with the key. Always allowed, whatever the key.

email

Only the buyer, who names the email the key was sold to.

code

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 code key is told try-again and cannot activate: use email there, 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 code for a key sent as one.
  • keygrant licenses get says 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

keygrant licenses revoke <key> [--reason <reason>] revokes the key. The app stops at its next check-in, or when its lease runs out offline.

  • --reason is refund, chargeback, subscription_canceled or manual.
  • --json prints { "ok": true }.

--reason default manual

keygrant licenses devices

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.
  • --json prints the API's answer, with each device's fingerprint too. See GET /v1/licenses/:key/activations.

Since 0.5.0

keygrant licenses free-device

keygrant licenses free-device <key> <activation id> frees that seat so another device can take it.

  • The activation id is the one keygrant licenses devices lists, the SDK's, and the device.activated webhook's.
  • --json prints { "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.
--product <id>

Required. The product's API ID.

--scenario <state>

valid, trial-ending, expired, revoked, device-limit or used-up.

  • device-limit fills its one seat, so your machine is refused.
  • used-up leaves its first meter no uses, so a spend is refused.

Default valid

--model <model id>

Its license model, else the product's first that issues keys.

  • trial-ending always takes the product's trial model, so it refuses --model.
  • used-up takes the first of those that counts uses; a model that counts none is no-meters.
--version <version>

The version it is for, else its first activation's.

--idempotency-key <key>

The same key on a retry answers the license first made.

--json

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:

--add <n>

Adds n uses on top of the key's own: a top-up by hand.

Range 1 to 1,000,000,000

--set <n>

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.

Range 0 to 1,000,000,000

--offline <n|default>

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.

--resets <never|period>

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.

Since 0.5.0

--reset

Starts a new period now, on a meter that comes back each billing period.

--remove

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 as no-meter.

Since 0.5.0

--idempotency-key <key>

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.

  • --add and --reset send 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; --json prints 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.

n

The uses to spend.

Default 1 ; Most 1,000,000,000

--id <id>

The spend's id, one made for each run otherwise. The same id again answers as the first spend did and spends nothing more.

Form 16 to 64 letters, digits, hyphens and underscores

--json

Prints the API's answer, { "meter": { ... } }, without the spend's id.

  • It is exact and whole or none: when fewer than n are left it exits 1 with used-up and 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.

--out <path>

Where to save it. By default it is saved under its own name, <product>-license.kglicense, in the current folder.

--force

Replace a file that is already there. Without it the command refuses.

--resign

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.

--json

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 a lease).
  • 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.
--key <file>

Required. The key set keygrant pubkey prints, the pubkey route's answer, or one JWK.

--product <id>

Also require it to be this product's.

--at <time>

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.

--json

Prints { "valid", "reason"?, "claims"? }, and for a file the kid its header names.

A lease is checked for, in this order:

  1. its signature, by the key it names (each key in turn for a lease that names none);
  2. its claims;
  3. 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:

  1. the text must be valid UTF-8 with one token (notes on lines starting with # are ignored);
  2. the key id it names must be in the key set;
  3. its signature must be by that key;
  4. 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.

FormMeans
2027-01-31A date: through that whole day, in UTC. For verify --at, its first moment.
2027-01-31T12:00:00ZA date and time in ISO 8601, with its offset.
1801396800000Milliseconds 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 --help says when it prints an object of its own: login, logout, verify, licenses file, a filtered products, and every page of licenses list --all.
  • When something fails, --json prints 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 as keygrant: <sentence>. When a command was named, a line follows saying to run that command's --help.
Exit codeMeans
0Done
1KeyGrant refused the request (the code says why), a lease or license file is not valid, or something else failed
2The command line is wrong, a file it names cannot be read or written, or licenses file would replace a file without --force
3No API key, a key KeyGrant does not accept, or no credential store for login
4Rate limited
5KeyGrant 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 second 429 exits 4 with the wait.
  • A 429 asking for more than two minutes is not waited out: it exits 4 at once.
  • --no-retry exits 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, and licenses uses with --add or --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 (idempotencyKey under --json). Run the command again with --idempotency-key and that key.
  • licenses extend sends the new expiry as an instant, so running it again with --to changes nothing more.
  • licenses uses and licenses use retry safely too: see keygrant licenses uses and keygrant licenses use.

Options every command takes

--json

Print the answer as JSON (see For scripts).

--api-url <url>

KeyGrant's address; KEYGRANT_API_URL too. https:// only, or http:// for localhost, 127.0.0.1 or [::1].

Default https://api.keygrant.dev

--no-retry

Do not wait out a 429: exit 4 at once.

-h, --help

The command's help.