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:
- A Keysat installation running on your Start9 — see Install & setup.
- BTCPay Server connected to Keysat — ditto.
- At least one product defined in the admin UI.
Pick an SDK
Three official SDKs ship today. They are wire-compatible — a license issued by your Keysat verifies identically in any of them.
# 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.
import { Verifier, PublicKey } from '@keysat/licensing-client'; const verifier = new Verifier( PublicKey.fromPem(ISSUER_PEM) ); const result = verifier.verify(licenseKeyFromUser); // Now decide what to do with the result, based on YOUR business model. // One-time purchase to use the app at all? Refuse to start unless valid. // Free + paid features? Check entitlements per feature. // Supporter badge only? Just render differently when valid. if (result.valid) { app.licensed = true; app.entitlements = result.entitlements; }
The verifier returns a result object with the following fields:
| Field | Type | Meaning |
|---|---|---|
valid | bool | Signature checked, expiry not exceeded. |
product_id | string | The product slug this license was issued for. |
policy_slug | string | Which policy was active at issue time. |
license_id | string | UUID of the license; useful for support tickets. |
issued_at | Date | UTC timestamp. |
expires_at | Date | null | null for perpetual. |
is_trial | bool | Set by the policy at issue time. |
seats | int | Max machines (0 = unlimited). |
entitlements | Set<string> | Feature flags baked into the signed payload. |
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:
try { const result = verifier.verify(licenseKey); if (result.valid) grantAccess(result); else showRenewalPrompt(result.expires_at); } catch (e) { if (e instanceof SignatureError) showTamperWarning(); else if (e instanceof FormatError) 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 runs on a cadence (e.g. once a week) against your Keysat’s revocation feed:
// Optional. Run on a cadence, ignore network errors. async function checkRevocation(licenseId: string) { const r = await fetch(`https://your-keysat.example/v1/licenses/${licenseId}/status`); if (r.ok) { const j = await r.json(); if (j.status === 'revoked') disableApp(); } }
You decide the policy. Many indie developers ship no revocation at all. Once a key is sold, it stays valid — refunds happen offline via BTCPay. 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.
| Method | Path | Use |
|---|---|---|
GET | /v1/products | List products (public). |
POST | /v1/admin/products | Create a product. |
POST | /v1/admin/policies | Create a policy. |
POST | /v1/admin/discount-codes | Create a discount or comp code. |
GET | /v1/admin/licenses/search | Find licenses by email, npub, or invoice. |
POST | /v1/admin/licenses/<id>/revoke | Revoke a license. |
POST | /v1/admin/webhook-endpoints | Register an outbound webhook. |
GET | /v1/admin/audit | Read audit log. |
POST | /v1/redeem | Redeem a free-license code (public). |
Full schemas for each endpoint live in Wire format & API reference.