Documentation
The KeyStack SDK is a tiny, dependency-free TypeScript package that handles request signing, retries, and error mapping so you can focus on your product.
Install
npm install @keystack/client
# or
pnpm add @keystack/clientConfigure
Create a client once and reuse it. The public key (starts with ak_live_) goes in any environment; the secret key must stay on a trusted server — never ship it to a browser.
import { KeyStack } from '@keystack/client';
const ks = new KeyStack({
publicKey: process.env.KEYSTACK_PUBLIC_KEY!,
secretKey: process.env.KEYSTACK_SECRET_KEY!,
// Optional — defaults to https://api.keystack.dev
baseUrl: 'https://api.keystack.dev',
// Optional — surfaces in our server logs to make support easier
appLabel: 'my-product/1.4.2',
});Validate a license
The most common call. Idempotent — the SDK will retry on 5xx/429 automatically.
const verdict = await ks.licenses.validate({
key: 'KS-ABCDE-FGHJK-MNPQR-STVWX-7C9F',
fingerprint: 'machine-uuid', // optional, but recommended for per-device cache hits
});
if (!verdict.valid) {
switch (verdict.reason) {
case 'expired': /* prompt renewal */ break;
case 'frozen': /* show "subscription paused" */ break;
case 'revoked': /* boot the user out */ break;
case 'not_found': /* probably a typo */ break;
}
}The returned envelope is fully typed — verdict.plan?.features carries any per-plan flags you've configured.
Activate, heartbeat, deactivate
For per-device licensing (typical desktop apps, IDE plugins):
// On install / first launch:
const { activationId } = await ks.devices.activate({
key,
fingerprint: deviceId,
hostname: os.hostname(),
os: process.platform,
});
// Periodically, while the app is open:
setInterval(() => {
ks.devices.heartbeat({ key, fingerprint: deviceId }).catch(console.warn);
}, 30 * 60 * 1000);
// On logout / uninstall:
await ks.devices.deactivate({ key, fingerprint: deviceId });Issue licenses programmatically
For billing webhooks. Requires an API key with FULL or ISSUE_ONLY scope.
const issued = await ks.licenses.issue({
planCode: 'PRO',
customer: { email: 'jane@acme.io' },
expiresAt: new Date(Date.now() + 365 * 86_400_000).toISOString(),
maxActivations: 3,
});
// The plaintext key is shown ONCE — send it to your customer right now.
await sendLicenseEmail(issued.customer?.email, issued.key);Errors
Every failure throws a typed exception with a stable code. Match on those, not on message.
import { isKeyStackError } from '@keystack/client';
try {
await ks.devices.activate({ key, fingerprint });
} catch (err) {
if (isKeyStackError(err) && err.code === 'license/activation-limit') {
return showUpgradePrompt(err.message);
}
throw err;
}KeyStackError has .code, .message, .statusCode, .requestId, and optional .details. The requestId is the same id we log on our side — paste it into a support ticket and we can trace your exact request.
Network failures throw KeyStackNetworkError instead; config issues (missing keys, missing fetch) throw KeyStackConfigError.
Runtime support
| Runtime | Status |
|---|---|
| Node 20+ | ✓ first-class |
| Cloudflare Workers | ✓ uses Web Crypto natively |
| Vercel Edge | ✓ |
| Deno | ✓ |
| Bun | ✓ |
| Browsers | Only via a backend proxy — see below |
Browsers
The SDK needs the secret key to HMAC-sign each call, so don't ship it to the browser. The recommended pattern is:
- Run the SDK from your backend (any runtime above).
- Expose a slim endpoint of your own (e.g.
POST /api/license/validate) that takes only the license key + a session-bound user id, calls the SDK server-side, and returns a redacted verdict. - Hit that endpoint from the browser.
Source
packages/client — MIT licensed, zero runtime dependencies.