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.json2. 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"
done4. 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.
- Machine fingerprints are regenerated on first activation against Paperkey. Existing Keygen machine slots are translated to "available capacity" on the license, not pre-bound to specific machines.
- Custom Keygen webhooks are not auto-replayed. Re-subscribe in the Paperkey dashboard with the same target URLs; we will resend on every state change going forward.
- Complex entitlements (per-feature flags) come over as JSON metadata on the license. The SDK exposes them with `license.metadata.<key>`. Refactor your feature gates accordingly.
- License analytics history (charts, time series) does not migrate. The Paperkey audit log starts on the day of import.
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