License keys & devices
Use license keys when your software runs somewhere you do not control: desktop apps, CLIs, plugins, self-hosted software or APIs where customers authenticate with a key. For web apps where users sign in, checking entitlements by user ID is usually simpler.
Key format
KV-7K2QD-9XWMN-4RT8H-PZ3VB
That is 20 random characters, or 100 bits of entropy, from an alphabet without I, L, O and U. Input is normalized: lowercase, extra spaces and the look-alikes O→0, I/L→1 are accepted.
Issuing keys
- Automatically from Stripe: every subscription creates a license (see Stripe integration).
- In the dashboard: Licenses → New license, or Grant a license on a customer page. Typical uses are beta testers, lifetime deals, partners and support credits.
- From your server:
const license = await keyvane.licenses.create({
plan: 'pro',
customer: user.id, // optional; omit for a standalone key
durationDays: 365, // or expiresAt: '2027-12-31'
maxActivations: 3, // devices; defaults to the plan's device limit
note: 'AppSumo #1234', // internal, never shown to publishable keys
});
console.log(license.key);
Validating in your app
Ship a publishable key (kv_pk_…) inside the app. It can only validate, activate and deactivate keys, so extracting it from your binary does not help anyone.
import { Keyvane } from '@keyvane/sdk';
const keyvane = new Keyvane({
apiKey: 'kv_pk_…',
baseUrl: 'https://keyvane.dev',
});
const result = await keyvane.licenses.validate({ key: licenseKey, fingerprint: machineId });
if (result.valid) {
result.entitlements?.can('export'); // what the license unlocks
} else {
console.log(result.code, result.message); // e.g. 'expired', 'This license expired on 2026-12-01.'
}
| Code | Meaning |
|---|---|
valid |
Good to go |
not_found |
No such key in this project |
expired |
Past expires_at |
suspended / revoked |
Disabled by you |
subscription_canceled / payment_overdue / subscription_inactive |
The Stripe subscription behind the license no longer grants access |
product_mismatch |
The key belongs to another product (pass product to enforce this) |
not_activated |
You passed a fingerprint that is not activated for this key |
activation_limit_reached |
activate was called on a full license |
Device activations
Set a device limit on the plan (default for new licenses) or per license. Then, on first launch:
const activation = await keyvane.licenses.activate({
key: licenseKey,
fingerprint: machineId, // stable per device, see below
name: `${os.hostname()} (macOS)`, // shown in your dashboard
});
if (activation.code === 'activation_limit_reached') {
// ask the user to deactivate another device
}
activateis idempotent: activating the same fingerprint again just updates last seen.validatewith afingerprintalso checks that this device is activated.deactivatefrees a seat, e.g. when the user signs out or moves to a new machine. You can also remove devices on the license page in the dashboard.
Fingerprints. Use something stable and privacy-friendly: hash the hostname, platform, architecture and CPU model rather than sending raw hardware IDs. The license CLI example shows one approach.
Working offline
Validation is an online check. For desktop apps, cache the last successful result and allow a grace period when Keyvane is unreachable:
try {
const result = await keyvane.licenses.validate({ key, fingerprint });
saveCache({ validatedAt: Date.now(), valid: result.valid });
} catch (err) {
if (err instanceof KeyvaneConnectionError && cache && Date.now() - cache.validatedAt < 7 * DAY) {
// keep working offline for up to a week
} else throw err;
}
Signed, fully offline license files are on the roadmap.
Revoking, suspending, extending
| Action | Effect | Reversible |
|---|---|---|
| Suspend | Access stops immediately | Yes, with Reinstate |
| Revoke | Access stops permanently | No. Issue a new key instead. |
| Change expiry | Extend or shorten access | Yes |
| Change plan | Upgrade or downgrade a manual license | Yes (Stripe licenses follow the subscription) |
All of these are available in the dashboard and through the API (/v1/licenses/{id}/suspend, /reinstate, /revoke and PATCH /v1/licenses/{id}), and every change is recorded in the audit log.