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/client

Configure

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

RuntimeStatus
Node 20+✓ first-class
Cloudflare Workers✓ uses Web Crypto natively
Vercel Edge
Deno
Bun
BrowsersOnly 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:

  1. Run the SDK from your backend (any runtime above).
  2. 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.
  3. Hit that endpoint from the browser.

Source

packages/client — MIT licensed, zero runtime dependencies.