Keysat Docs
Get started · Integrate the SDK

Integrate the SDK.

Wire Keysat licenses into your software in under an afternoon. The verifier is pure-function, offline, and ships in five lines. What you do with the result (refuse to start without a license, unlock specific features, just show a "supporter" badge) is your call. The SDK is the primitive; the business model is yours.

Prerequisites

Before you start, you should have:

Pick an SDK

Four official SDKs ship today. They are wire-compatible. A license issued by your Keysat verifies identically in any of them. Cross-check fixtures in the daemon repo prove each SDK accepts the same bytes the daemon mints.

# npm
npm install @keysat/licensing-client

# pnpm
pnpm add @keysat/licensing-client

If your language isn’t covered, see Wire format. The format is small and porting takes about an afternoon.

Step 1: Embed your public key

In the admin UI, open Overview and copy the issuer public key from the "Embed your public key" card. (Or fetch it from GET /v1/issuer/public-key.) Paste it into your application’s source code as a compile-time constant.

const ISSUER_PEM = `-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAmz7q8r4t1v…h3k2pXq9wL
-----END PUBLIC KEY-----`;

Embed it. Don’t fetch it. The whole point of offline verification is that your software can’t be tricked by a network-level attacker. If you fetch the public key at runtime, you’re back to trusting a server.

Step 2: Verify a license at startup

Read the user’s license key from wherever you store it (a file in their data directory, the OS keychain, an env var) and verify it on application start. In a server-side app the key arrives per request instead: read it from a header you define (for example X-License-Key) or the session, then verify it the same way.

import { Verifier, PublicKey } from '@keysat/licensing-client';

const verifier = new Verifier(
  PublicKey.fromPem(ISSUER_PEM)
);

// verify() returns the verified license, or THROWS if the key is missing,
// malformed, forged, or signed by someone else. Catch it (see step 3).
const license = verifier.verify(licenseKeyFromUser);

// What you do next is up to YOUR business model. The verified payload
// carries the entitlements baked in at issue time.
app.licensed = true;
app.entitlements = license.payload.entitlements; // string[]

On success, verify() returns a VerifyOk result. There is no valid boolean: an invalid key throws (TS / Python) or returns Err (Rust). See step 3. Field names are camelCase in TS/JS and snake_case in Rust/Python.

FieldTypeMeaning
productIdstringUUID of the product this license was issued for.
licenseIdstringUUID of the license; useful for support tickets.
payload.entitlementsstring[]Feature slugs baked into the signed payload.
payload.issuedAtnumberUnix seconds at issue time.
payload.expiresAtnumberUnix seconds; 0 for perpetual.
payload.isTrialboolSet by the policy at issue time.
payload.isFingerprintBoundboolTrue if the key is bound to one machine.

verify() checks the signature and format, not expiry or revocation. A perpetual license never expires; to reject expired keys offline, compare the payload’s expiresAt to now. Every SDK ships an isExpiredAt/is_expired_at helper for this; TS and Rust also offer a one-call verifyWithTime(key, nowUnixSeconds). Live status (revoked, suspended, seats in use, the policy slug) isn’t in the offline payload; get it from the online validate path below.

Step 3: Handle errors gracefully

Verification can fail for benign reasons (the user hasn’t pasted a license yet) or hostile ones (someone tampered with a license file). Distinguish them in your UX:

import { LicensingError } from '@keysat/licensing-client';

try {
  const license = verifier.verify(licenseKey); // throws if not valid
  grantAccess(license);
} catch (e) {
  // Every failure is a LicensingError with a machine-readable .code:
  // 'bad_signature' (tampered / forged), 'bad_format' or 'bad_encoding'
  // (garbled input), 'bad_version', 'expired' (only from verifyWithTime).
  if (e instanceof LicensingError && e.code === 'bad_signature') showTamperWarning();
  else if (e instanceof LicensingError) showInputError();
  else showGenericError(e);
}

Renewals & revocation

Keysat licenses are signed at issue time and do not phone home. If a license is revoked in the admin UI, the existing key continues to verify in your app. That’s the trade-off for offline.

If you need revocation, ship a thin online check that re-validates the key on a cadence (e.g. once a week) against your Keysat’s POST /v1/validate. A revoked license returns ok: false with reason: "revoked":

// Optional. Run on a cadence, ignore network errors.
async function checkRevocation(licenseKey: string) {
  const r = await fetch('https://your-keysat.example/v1/validate', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ key: licenseKey }),
  });
  if (r.ok) {
    const j = await r.json();
    if (!j.ok && j.reason === 'revoked') disableApp();
  }
}

You decide the policy. Many indie developers ship no revocation at all. Once a key is sold, it stays valid. That’s perfectly reasonable.

Admin API

The admin UI is a thin shell over a small JSON API. Bearer-auth all requests with your admin API key.

MethodPathUse
GET/v1/productsList products (public).
POST/v1/admin/productsCreate a product.
POST/v1/admin/policiesCreate a policy.
POST/v1/admin/discount-codesCreate a discount or comp code.
GET/v1/admin/licensesList a product’s licenses; requires ?product_id=<uuid>.
GET/v1/admin/licenses/searchSearch licenses by buyer_email, nostr_npub, or invoice_id.
POST/v1/admin/licenses/<id>/revokeRevoke a license.
POST/v1/admin/webhook-endpointsRegister an outbound webhook.
GET/v1/admin/auditRead audit log.
POST/v1/redeemRedeem a free-license code (public).

Full schemas for each endpoint live in Wire format & API reference.