Skip to main content

Migrating from Keygen

Keygen is a mature, licensing-only API with a JSON:API surface, rich policies (including floating seats and MAINTAIN_ACCESS), and cryptographically signed license files. Licensr is a simpler REST API that bundles payments, a branded buy page, and a customer portal on top of seat/domain activation.

Use this guide if you already integrate Keygen and want to move validation (and optionally checkout) to Licensr.

Concept map

KeygenLicensrNotes
ProductPlugin (plugin_slug)Activation mode (seat / domain) is fixed at creation.
PolicyPlanPricing + seat cap + optional feature_flags + perpetual-fallback toggle.
LicenseLicense (lic_...)Same idea; status is active / inactive / expired.
MachineActivation (seat)Domain-mode activations cover WordPress/web hosts Keygen doesn't model natively.
Entitlement resourcesPlan feature_flags JSONReturned on every validate/activate/token — no separate entitlement list endpoint.
Signed / encrypted license fileEdDSA JWT from POST /v1/license/tokenVerify against the per-plugin JWKS. Shorter TTL, simpler verify path.
MAINTAIN_ACCESS / expiration strategyPerpetual fallback on the planExpired → valid:false, entitlement:"limited", fallback:true.
Floating / concurrent licensesNot supportedLicensr is node-locked.
Your own Stripe / billingBuilt-in checkout on your Stripe / Mercado Pago / PayPal/buy/your-slug + subscription lifecycle included.

See the full side-by-side at licensr.app/vs/keygen.

Replace JSON:API validate with one REST call

Keygen's validate-key / license-actions flow becomes:

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

const client = new LicensrClient({
apiKey: 'pk_live_...',
pluginSlug: 'my-plugin',
});

const result = await client.validate({licenseKey: userEnteredKey});
if (result.valid) {
unlockFullFeatures(result.featureFlags);
} else if (result.entitlement === 'limited') {
unlockDegradedMode(); // perpetual-fallback plan
}

Raw equivalent:

POST /v1/license/validate
Authorization: Bearer pk_live_...
Content-Type: application/json

{"license_key":"lic_...","plugin_slug":"my-plugin"}

There is no JSON:API compound document, no included array, and no separate "license action" resource. Always branch on valid / entitlement — an unknown key returns 200 {valid: false}, not 404.

Machines → seat activations

await client.activate({
licenseKey: userEnteredKey,
activationType: 'seat',
identifier: stableHwid, // reuse as deviceId for rate-limit bucketing
label: 'Build machine',
});

Deactivate via client.deactivate({licenseKey, activationId}) or let the customer do it in the branded portal. Fingerprint / component hashes from Keygen don't transfer — pick a stable host identifier (see §7 Identifier hygiene).

Entitlements → feature flags

Keygen entitlement resources become a single JSON object on the plan:

{
"pro_export": true,
"seat_limit_ui": 3
}

Returned as feature_flags on validate/activate/token. Prefer checking flags in-process over round-tripping a separate entitlement API.

Offline: license files → JWTs

Keygen's signed/encrypted license files are replaced by short-lived EdDSA JWTs:

  1. client.token({licenseKey}) while online.
  2. Persist the token (JS OfflineTokenStore, C++ FileTokenStore, C# IOfflineTokenStore).
  3. Verify locally against the JWKS URL on next launch.
import {verifyOfflineToken} from '@licensr/sdk';

const {token, jwksUrl} = await client.token({licenseKey});
const claims = await verifyOfflineToken(token, jwksUrl);

No RSA path and no encrypted-file scheme — Ed25519 verify only. Re-validate online before exp; offline tokens carry no live revocation signal.

Expiration strategies → perpetual fallback

If you relied on Keygen's MAINTAIN_ACCESS (or similar) so expired licenses keep a degraded tier:

  1. Enable perpetual fallback on the Licensr plan.
  2. On validate, read entitlement === 'limited' + fallback === true (status stays "expired", valid stays false).
  3. Older clients that only check valid continue to fail closed.

Deliberate cutoffs (refund, admin deactivate) always return entitlement: "none".

What you gain that Keygen doesn't ship

  • Hosted buy page and automatic key email on your own payment account.
  • Subscription lifecycle wired to license state — no custom Stripe webhook worker.
  • Branded customer portal for seats and billing.
  • Lower entry price for the same validate/activate core (see pricing).

Migration checklist

  1. Create the plugin + plans; encode Keygen entitlements as feature_flags and map expiration strategy → perpetual fallback.
  2. Issue a client-scoped pk_live_... and embed it via an official SDK (or raw REST).
  3. Replace JSON:API validate/machine calls with /validate + /activate.
  4. Swap offline license-file verify for JWT + JWKS.
  5. Optionally move new checkout to /buy/your-slug.
  6. Re-issue licenses for the existing cohort — Keygen keys are not portable.
  7. Run both systems in parallel until activations tip over.

Further reading