ReferenceUpdated 2026-11-05View as MarkdownEdit on GitHub

JavaScript / TypeScript SDK

@keyvane/sdk is the official, MIT-licensed SDK. It has zero dependencies, ships TypeScript types, and runs on Node.js 18+, Bun, Deno, edge runtimes and browsers (with a publishable key).

npm install @keyvane/sdk

Configure

import { Keyvane } from '@keyvane/sdk';

const keyvane = new Keyvane({
  apiKey: process.env.KEYVANE_SECRET_KEY,     // kv_sk_… on servers, kv_pk_… in apps
  baseUrl: 'https://keyvane.dev',     // your Keyvane deployment
  timeoutMs: 10_000,   // default
  maxRetries: 2,       // default, for safe requests only
  cacheTtlMs: 0,       // cache entitlements.get() results (default off)
});

With no options, apiKey falls back to KEYVANE_SECRET_KEY (or KEYVANE_PUBLISHABLE_KEY) and baseUrl to KEYVANE_URL.

Inputs use camelCase (externalId, idempotencyKey). Responses mirror the REST API exactly (snake_case), so the API reference doubles as the type reference.

Entitlements

const access = await keyvane.entitlements.get({ customer: user.id, product: 'pdf-analyzer' });

product is optional when your project has one product. The result is an Entitlements object:

Member Returns
access.plan { id, key, name } or null
access.source 'license', 'default_plan' or 'none'
access.can(key, quantity = 1) true if the feature is included and, for metered features, quantity units remain
access.remaining(key) Units left this period. null = unlimited, 0 = not included
access.limit(key) / access.used(key) The metered limit / units used this period
access.isOnPlan('pro', 'business') Plan membership check
access.feature(key) The raw feature entitlement (with resets_at, unit, …)
access.subscription, access.license Billing context, when access comes from a license
access.data The full API response

Server-side single-feature check (handy from other languages or edge functions):

const check = await keyvane.entitlements.check({ customer: user.id, feature: 'export' });
check.allowed; // boolean
check.code;    // 'ok' | 'not_included' | 'limit_exceeded'

Usage

const usage = await keyvane.usage.record({
  customer: user.id,        // or licenseKey: 'KV-…'
  feature: 'pdf_analysis',
  quantity: 1,              // default 1
  idempotencyKey: job.id,   // strongly recommended
});

if (!usage.allowed) showUpgrade(usage.message); // usage.code: 'limit_exceeded' | 'not_included' | 'license_invalid'

record never throws because a limit was reached; that is a normal outcome. It only throws for real errors (invalid key, unknown feature, network). Recording usage invalidates cached entitlements for that customer.

Charge only for successful work. Either record first and accept that a failed job consumed a unit, or check with access.can(feature) first and record after the work succeeded (two concurrent requests may then both pass the check).

Customers

await keyvane.customers.upsert({ externalId: user.id, email: user.email, name: user.name, metadata: { plan_source: 'web' } });
await keyvane.customers.retrieve(user.id);            // by your ID or cust_…
await keyvane.customers.update(user.id, { email: 'new@example.com' });
await keyvane.customers.list({ limit: 20 });

Licenses

// Secret key
const license = await keyvane.licenses.create({ plan: 'pro', customer: user.id, durationDays: 365 });
await keyvane.licenses.retrieve(license.id);
await keyvane.licenses.list({ customer: user.id, status: 'active' });
await keyvane.licenses.update(license.id, { plan: 'business', maxActivations: 5 });
await keyvane.licenses.suspend(license.id, { reason: 'chargeback' });
await keyvane.licenses.reinstate(license.id);
await keyvane.licenses.revoke(license.id, { reason: 'refund' });

// Publishable or secret key
const result = await keyvane.licenses.validate({ key, fingerprint });
await keyvane.licenses.activate({ key, fingerprint, name: 'Studio Mac' });
await keyvane.licenses.deactivate({ key, fingerprint });

validate and activate return a LicenseValidation with valid, code, message, license, activation and entitlements (an Entitlements object when valid). See License keys.

Products

const { data: products } = await keyvane.products.list();
const product = await keyvane.products.retrieve('pdf-analyzer'); // plans, features, matrix

Useful for rendering a pricing table from the same source of truth as your access checks.

Errors

Every error extends KeyvaneError and carries status, code, type, param and requestId:

Class When
KeyvaneInvalidRequestError 400: missing or invalid parameter
KeyvaneAuthenticationError 401: missing, wrong or revoked key
KeyvanePermissionError 403: publishable key on a secret-key endpoint
KeyvaneNotFoundError 404: unknown customer, license, product, plan or feature
KeyvaneConflictError 409: e.g. reinstating a revoked license
KeyvaneRateLimitError 429
KeyvaneApiError 5xx
KeyvaneConnectionError Network failure or timeout

Retries and timeouts

Safe and idempotent requests are retried on network errors, 429 and 5xx with exponential backoff (honouring Retry-After): all GET requests, customers.upsert, license validation, activation and deactivation, and usage records that carry an idempotency key. Other writes are not retried automatically, because retrying them could duplicate work.

When Keyvane is unreachable: fail open or closed?

Decide per feature:

let access;
try {
  access = await keyvane.entitlements.get({ customer: user.id });
} catch (err) {
  if (err instanceof KeyvaneConnectionError) {
    // Fail closed for paid features, keep the free experience working:
    return renderFreeTier();
  }
  throw err;
}

A small cacheTtlMs (5–30 seconds) also absorbs brief network blips and cuts latency on busy pages.