The open-source command-line client for the Keylight management API —
think wrangler, but for your Keylight tenant: apps, key types, licenses, customers, and
payment integrations, from the terminal instead of the dashboard.
Every endpoint the API exposes (OpenAPI spec) is reachable from a command, and a coverage test fails the build if that stops being true.
Status: 0.x. Flag names and human-readable output may change in a minor release. The
--jsonoutput and the exit codes are the parts to script against.
Licensing shouldn't mean bolting a heavyweight, phone-home-or-die SDK onto your app.
- Works offline. The license is a signed lease your app verifies locally with Ed25519 — no network round-trip to gate a feature, no lockout when the machine is offline.
- Tamper-resistant by design. Entitlements live inside the signature; a forged or hand-edited lease can't pass verification without the tenant's private key.
- One command surface for the whole tenant. Apps, key types, licenses, customers, and payment integrations — every operation the dashboard exposes, scriptable from the terminal or a coding agent.
- RFC 8628 device-code login.
keylight loginprints a code, opens the browser to approve it, and works over SSH and on headless machines — the browser never needs to reach the CLI. - Confirm-gated by default. Destructive operations require an out-of-band browser approval, so a leaked token can't quietly delete anything.
brew tap keylight-dev/tap
brew trust keylight-dev/tap
brew install keylightNo Rust toolchain, and nothing to clear past Gatekeeper. The brew trust step is not specific
to this tap — since Homebrew 6, loading a formula from any third-party tap fails until you trust
it. The formula is 60 lines of Ruby
and worth reading before you trust it.
cargo install keylight-cliInstalls a keylight binary. Requires Rust 1.85+ (edition 2024). The crate is keylight-cli;
the binary is keylight.
Download from Releases for macOS (Apple Silicon), Linux (x86_64), or Windows (x86_64).
macOS: the release binaries are not yet code-signed or notarized, so Gatekeeper will block them on first run — clear the quarantine flag with
xattr -d com.apple.quarantine ./keylight. Homebrew andcargo installboth avoid this entirely, which is why they come first.
keylight login # approve in your browser
keylight products list # confirm it works
keylight licenses list --limit 10keylight login is the easy path. For CI and agents, set an environment variable instead.
keylight loginPrints a short code, opens your browser to approve it, and stores the token when you do. Uses RFC 8628 device authorization, so it works over SSH and on headless machines — the browser never needs to reach the CLI.
Requests every non-destructive permission by default. Narrow it with --scope, repeated:
keylight login --scope licenses:read --scope usage:readYou can also uncheck permissions on the approval page. danger (destructive operations) is
never requested by default and must be opted into there.
Check the code on the approval page matches the one in your terminal. That match is what proves the request came from the session you started, and it is the reason the flow is safe on a machine you do not control.
| Source | Notes |
|---|---|
KEYLIGHT_API_TOKEN |
Always wins. Nothing is read from or written to disk. Use this in CI. |
| Stored config file | Written by keylight login or keylight auth set-token. |
export KEYLIGHT_API_TOKEN=klm_...
keylight auth statusTo store a token by hand:
keylight auth set-token # paste, then Enter
echo "$KEYLIGHT_TOKEN" | keylight auth set-token # or pipe itRead from stdin, never a command-line argument — arguments land in shell history and are
visible to other users via ps. Stored at your platform's config directory
(~/.config/keylight/config.toml on Linux, ~/Library/Application Support/keylight/config.toml
on macOS) with 0600 permissions.
Create tokens from the dashboard under Settings → API tokens.
keylight auth status # which tenant, token, and scopes the current auth resolves to
keylight logout # revoke the token server-side and delete the local copylogout revokes server-side and removes the local file. If the revoke call fails — offline, or
the token was already revoked — the local copy is still deleted and the output says so, rather
than leaving a credential on disk you believe is gone.
keylight products list
keylight products get my-app
keylight products create --display-name "My App" --key-prefix MYAP --support-email help@myapp.com
keylight products update my-app --purchase-url https://myapp.com/buy
keylight products free-tier set my-app --enabled
keylight products test-mode disable my-appID NAME PREFIX FREE TIER TEST MODE
my-app My App MYAP true -
other Other App OTHR - true
A - means the API did not return a value for that column. It omits these flags entirely when
they are off rather than sending false, so an off flag reads as -, not false.
Enabling Stripe test mode needs two secrets, read from environment variables you name:
export KEYLIGHT_STRIPE_TEST_SECRET_KEY=sk_test_...
export KEYLIGHT_STRIPE_TEST_WEBHOOK_SECRET=whsec_...
keylight products test-mode enable my-appkeylight products key-types list --product my-app
keylight products key-types create --product my-app \
--key-type-id pro --display-name "Pro" --activation-limit 3 --duration-days 365
keylight products key-types update pro --product my-app --activation-limit 5
keylight products key-types delete pro --product my-app # needs browser approval
keylight products key-types verify-prices --product my-app--duration-days 0 means perpetual. Any other number is a real expiry in days.
keylight licenses list --product my-app --status active --limit 20
keylight licenses get <license-id>
keylight licenses create --product my-app --key-type pro \
--customer-email buyer@example.com --send-email
keylight licenses remint <license-id> --send-email
keylight licenses deactivate-device <license-id> --instance-id <instance>
keylight licenses revoke <license-id> # needs browser approvalKEY APP TYPE STATUS DEVICES EXPIRES
MYAP-****-****-****-7K2QP my-app pro active 1/3 2027-06-18
Raw license keys are never returned by the API — lists and detail views show the masked form.
The one exception is remint, which returns the plaintext replacement key once; the command
prints it and says so.
create and import always send an Idempotency-Key, so a retry after a timeout cannot issue
a second license to the same customer. Pass --idempotency-key to control it yourself.
Bulk import takes a JSON file — either an array of rows, or an object with a rows array:
keylight licenses import --product my-app --file rows.json --migration-source gumroadExport writes CSV to stdout so it composes, or to a file with --output:
keylight licenses export > licenses.csv # needs browser approval
keylight licenses export --output licenses.csvkeylight customers list --limit 20
keylight customers get <customer-id>
keylight customers create --email buyer@example.com --name "Buyer"
keylight customers link-license <customer-id> --license <license-id>
keylight customers entitlements set <customer-id> \
--license <license-id> --entitlements '["sync","export"]'A customer id is a SHA-256 hash of their email, so get takes the id that list prints.
keylight integrations list
keylight integrations status # Stripe connection detail
keylight integrations connect stripe --mode live # returns an OAuth URL to open
keylight integrations disconnect paddle --environment sandbox
keylight integrations rotate-secret paddle # needs browser approvalNon-Stripe providers take credentials from environment variables you name:
export PADDLE_KEY=pdl_live_...
keylight integrations connect paddle --environment live --api-key-env PADDLE_KEYkeylight webhook get
keylight webhook set --url https://example.com/keylight # needs browser approval
keylight webhook set --url "" # clears it; delivery stops
keylight sdk-key get # needs browser approval
keylight sdk-key rotate # needs browser approval
keylight usage --days 30
keylight test-purchase create --product my-app --key-type pro
keylight test-purchase get <run-id>webhook set returns the signing secret — use it to verify the signature on every delivery.
webhook get only reports whether a secret exists, never its value.
KEYLIGHT_SDK_KEY=sdk_live_... keylight setup verify-license --product my-app
keylight setup verify-license --cleanIssues a real test license, activates it through the Rust SDK, and verifies the
returned lease against the tenant keyset. It does not modify application code
or count as product usage. The check is identified as Setup check; a local,
owner-only state file retains its plaintext key only until --clean removes
the device and revokes the license.
Every command supports --json, which prints the raw API response — that is the contract to
script against, and it is stable in a way the human output is not.
keylight --json licenses list --product my-app | jq '.items[].displayKey'Errors follow the same rule: --json prints {"error": "..."} to stdout; otherwise the message
goes to stderr. Progress output (approval URLs, waiting notices) always goes to stderr, so
stdout stays parseable and redirects stay clean.
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Everything else |
2 |
Needs action: authenticate, or supply the missing setup input. |
3 |
A confirmation was denied or expired |
2 is distinct because it marks a failure an agent can act on without treating a
verification failure as something to work around.
Some operations are dangerous enough that a bearer token alone cannot run them — a human has to approve the specific call in a browser (same shape as npm's web-based 2FA). When a command hits one, the CLI:
- Prints an approval URL (and opens it, unless
--no-browseror stdout is not a terminal). - Polls until you approve or deny it, or it expires after 10 minutes.
- Retries the original call automatically once approved.
Seven operations are gated: licenses revoke, licenses export, products key-types delete,
integrations rotate-secret, sdk-key get, sdk-key rotate, and webhook set. You never have
to know which — the CLI walks the flow whenever the server asks.
These block and wait by design. They are human-gated operations, and failing fast for unattended callers would misrepresent what they are; an agent can surface the URL to its user.
| Env var | Effect |
|---|---|
KEYLIGHT_API_TOKEN |
Bearer token; wins over the stored config file. |
KEYLIGHT_API_URL |
API base URL (default https://api.keylight.dev). For local dev and staging. |
Global flags: --json, --no-browser (never auto-open a browser; URLs are still printed),
--api-url.
No command ever requires an interactive prompt. Every input is available as a flag, so an
agent or CI job can drive the whole surface. Secrets are the one exception to "as a flag": they
are read from environment variables you name, because argv lands in shell history and is visible
to other users via ps.
Pagination is explicit. list commands take --limit and --cursor and print the next
cursor when one exists. Nothing auto-pages — one command means one request, and a silent loop
over a large tenant is both surprising and expensive.
--product is required wherever the endpoint is product-scoped, never inferred from config.
Inferring it would make the same command mean different things on different machines.
Commands are hand-written, not generated from the OpenAPI spec. Help text, flag names, output
formatting and error messages are the entire value over curl. Drift protection comes from the
coverage test instead, which catches the failure that actually matters — an endpoint no command
can reach.
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo testTests mock the HTTP transport (no network needed) and isolate the config path, so cargo test
never reads or writes your real config.
The coverage test reads a checked-in snapshot of the API spec at tests/fixtures/openapi.json
rather than fetching it, so the suite is deterministic and works offline. Refresh it when the
API changes:
scripts/refresh-openapi.shDrift then shows up as a reviewable diff instead of a surprise failure, and the test reports any endpoint that no command reaches.
keylight-swift— Swift SDKkeylight-rust— Rust SDK + Tauri pluginkeylight-js— TypeScript/JavaScript SDKkeylight-csharp— C#/.NET SDK
Keylight is the licensing layer for desktop apps. You keep your own Stripe account, your own pricing, and your own customers — Keylight issues the licenses and tells your app who is allowed to run it. This CLI is how you manage all of that from a terminal instead of the dashboard.
- License keys issued automatically when a payment completes
- Device activations with limits you set, and self-serve deactivation
- Offline validation — signed Ed25519 leases your app verifies locally
- Feature entitlements signed into the lease, so tiers work offline too
keylight.dev · Documentation · Pricing
Apache-2.0 — see LICENSE.