Skip to main content

JavaScript / TypeScript SDK

@licensr/sdk wraps the licensing API: validation, seat/domain activation, offline EdDSA token verification, and hosted checkout. Ships as dual ESM/CJS with bundled types, zero runtime dependencies beyond jose (used only for offline token verification).

Install​

npm install @licensr/sdk

Quickstart​

From Admin → Products → your product:

  • pluginSlug is the product slug
  • apiKey is an API key on that product — issue a Client key to embed in a SPA or shipped app (it cannot open checkout)
import {LicensrClient} from '@licensr/sdk';

const client = new LicensrClient({
apiKey: 'pk_live_...', // Client key — safe to embed in a shipped app
pluginSlug: 'my-plugin',
});

const result = await client.validate({licenseKey: userEnteredKey});
if (result.valid) {
unlockFullFeatures();
} else if (result.entitlement === 'limited') {
unlockDegradedMode(); // perpetual-fallback: expired, but the plan grants a limited tier
}

The result keeps what you act on at the top level: valid, status (why, for your message), entitlement and featureFlags. Plan and limit details are grouped under result.license (planId, activationMode, maxSeats, maxDomains, seatsInUse, expiresAt). They are for display only; never use them to decide whether to unlock.

See embedded-key doctrine for what an extracted pk_live_... can and cannot do.

Browser vs Node​

RuntimeWhat to configure
Node / your backendNo CORS. On the product set Client type → Native. Keep the key in an env var if you prefer not to ship it.
Browser (React, Vite, etc.)The browser sends Origin and a CORS preflight. On the product set Client type → Browser and optionally add this page's address under Advanced options → Allowed websites — scheme + host + port, no path (http://localhost:3010, not localhost alone). Empty allows any website; the API key is still required.

If a browser call fails as LicensrNetworkError / "Failed to fetch", the preflight or response was blocked — check Allowed websites first, then that you are using a Client key on that same product.

First launch (typical)​

const {activationId} = await client.activate({
licenseKey,
activationType: 'seat',
identifier: stableMachineId, // reuse as deviceId on the client
label: 'Studio iMac',
});
// Save activationId: you need it to deactivate later.

const result = await client.validate({licenseKey});
if (!result.valid && result.entitlement !== 'limited') throw new Error('Invalid license');

Seat activationType must match the product (domain plugins use 'domain' and a hostname). Activate is idempotent for the same identifier.

API​

Every method returns a camelCase-mapped response and throws on failure — see Errors.

MethodWrapsNotes
client.validate({licenseKey})POST /v1/license/validateAlways read valid/entitlement; never branch on HTTP status — an unknown key returns 200 {valid: false}, not 404.
client.token({licenseKey})POST /v1/license/tokenSame check as validate, plus a short-lived signed offline token. See Offline verification.
client.activate({licenseKey, activationType, identifier, label?})POST /v1/license/activateactivationType is 'seat' (per-machine) or 'domain', fixed by the plugin's configured mode.
client.deactivate({licenseKey, activationId})POST /v1/license/deactivateFrees a seat/domain slot.
client.activations({licenseKey})GET /v1/license/activationsLists every current activation.
client.checkout({planId, customerEmail, successUrl?, cancelUrl?})POST /v1/billing/checkoutReturns checkoutUrl — redirect the user there. Requires a key with the checkout scope.

Client options​

new LicensrClient({
apiKey: 'pk_live_...',
pluginSlug: 'my-plugin',
baseUrl: 'https://api.licensr.app', // default; override for self-hosted/staging
deviceId: stableHwid, // buckets rate limits per installation instead of per key — reuse your activate() identifier
origin: 'app://my-plugin', // browsers send Origin themselves; native apps: set client type Native in admin instead
timeoutMs: 10_000,
retry: {maxRetries: 2, baseDelayMs: 300, maxDelayMs: 5000}, // network errors / 429 / 5xx, exponential backoff + jitter, honors Retry-After
});

Events​

client.events.on('validated', (result) => console.log('validated', result));
client.events.on('retry', ({method, attempt, delayMs}) =>
console.log(`retrying ${method}, attempt ${attempt} in ${delayMs}ms`)
);
client.events.on('error', ({method, error}) => reportToCrashlytics(method, error));

Available events: validated, activated, deactivated, tokenIssued, retry, error.

Offline verification​

client.token() mints an EdDSA-signed JWT whose claims mirror validate()'s response. Verify it fully offline (no network beyond the first JWKS fetch, which is cached for the process lifetime):

import {verifyOfflineToken} from '@licensr/sdk';

const {token, details} = await client.token({licenseKey});
const claims = await verifyOfflineToken(token, details.jwksUrl);
// claims.valid, claims.entitlement, claims.exp, ...

Offline tokens carry no revocation signal — they prove the token was genuinely issued and hasn't expired, not that the license is still active right now. Re-validate online before details.expiresAt.

To boot fully offline (no network at all, e.g. on first launch before any successful /token call), persist the last-known-good token yourself using the OfflineTokenStore interface (an InMemoryOfflineTokenStore is included, but doesn't survive a restart — back it with localStorage, a config file, or your platform's keychain):

import {InMemoryOfflineTokenStore, verifyOfflineToken} from '@licensr/sdk';

const store = new InMemoryOfflineTokenStore(); // swap for your own persistent implementation
// details.jwksUrl from your last online token() call. It does not change, so save it next to the token.
const jwksUrl = savedJwksUrl;

const cached = store.get(licenseKey);
if (cached) {
try {
await verifyOfflineToken(cached.token, jwksUrl); // still valid — usable while offline
} catch {
/* expired or tampered — fall through to an online check */
}
}

Errors​

ClassWhen
LicensrApiErrorNon-2xx response. Has status, code (matches the error codes in the License calls guide), message, and retryAfterSeconds. Branch on code, not message.
LicensrNetworkErrorThe request never got a response (network failure, timeout, retries exhausted). In a browser this is also how CORS failures surface — see Browser vs Node.
LicensrTokenVerificationErrorverifyOfflineToken rejected a bad signature, wrong algorithm, or expired token.

Source & issues​

Source lives in github.com/tekunodev/licensr-js, mirrored from this monorepo's sdks/js/. File issues and PRs there.