Migration guide

Move from Keygen to Paperkey in 10 minutes.

You export your existing licenses from Keygen, transform the JSON to the Paperkey shape, POST them in. The SDK call site in your shipped app stays the same. The dashboard, the webhooks, and the audit log come up populated on the first reload.

~10 min for under 500 licenses · ~1 h for under 10k · scripted for 10k+

Concept mapping

Most concepts map one-to-one. The names differ in three places (Account, Policy, Token).

Keygen Paperkey Note
Account Workspace (account) Top-level isolation. One per company.
Product Product Same idea. Carries config (cap, fingerprint policy).
Policy Product config Inlined into the product. Per-license overrides on the License.
License License Same. Arbitrary key formats accepted on import.
Machine Activation Same. Hashed fingerprint, slot of cap.
Token (license token) Validation token JWT issued by validate. Compatible drop-in shape.
Webhook Webhook event HMAC-SHA-256, hex signature, 5s timeout.
Entitlement License metadata Stored as JSON on the license. Read by the SDK.

Four steps

1. Export from Keygen

Use the Keygen API to dump every license into a single JSON file. Replace <account-id> and <keygen-api-token> with your values.

curl -X GET 'https://api.keygen.sh/v1/accounts/<account-id>/licenses' \
  -H 'Authorization: Bearer <keygen-api-token>' \
  -H 'Accept: application/vnd.api+json' \
  > keygen-licenses.json

2. Transform to the Paperkey shape

A small Node one-liner. Reads keygen-licenses.json, projects every license into the Paperkey input format, writes paperkey-licenses.json. No external dependencies.

node -e '
  const data = JSON.parse(require("fs").readFileSync("keygen-licenses.json", "utf8"));
  const out = data.data.map(l => ({
    key: l.attributes.key,
    customerEmail: l.relationships?.user?.data?.id ?? null,
    expiresAt: l.attributes.expiry,
    activationCap: l.attributes.maxMachines ?? 1,
    metadata: l.attributes.metadata ?? {},
  }));
  require("fs").writeFileSync(
    "paperkey-licenses.json",
    JSON.stringify(out, null, 2)
  );
'

3. Import into Paperkey

POST each license to /v1/licenses. Plain bash + jq. The endpoint is idempotent on the key, so you can re-run safely if a request times out.

jq -c '.[]' paperkey-licenses.json | while read -r row; do
  curl -X POST https://api.paperkey.dev/v1/licenses \
    -H "Authorization: Bearer pk_<your-api-key>" \
    -H 'Content-Type: application/json' \
    -d "$row"
done

4. Update the SDK config

In your shipped app, point the SDK at api.paperkey.dev with the new public verification key. The SDK call site stays the same. Customers re-activate seamlessly the next time their app phones home.

import { Paperkey } from '@paperkeyhq/sdk';

const pk = new Paperkey({
  apiUrl: 'https://api.paperkey.dev',
  productId: 'prod_<your-product-id>',
  publicKey: '<your-paperkey-public-verify-key>',
});

// Same call site as before. The SDK handles cache + offline grace.
const verdict = await pk.validate(licenseKey);

Known limitations

Honest list. We tell you upfront so you do not discover them in production.

Need a hand?

If you have more than 10k licenses, complex entitlements, or just want eyes on the migration, write to us with your Keygen account size and a contact time. We do walkthroughs in 30 minutes against your real data, free.

Email hello@paperkey.dev

Still evaluating?

Read the 5-minute quickstart. Or skip it: the SDK has decent defaults and the dashboard explains itself.