New: Offline-first licensing with cryptographic validation. Learn more

Migrate from Keygen

A comprehensive guide to migrating your licensing system from Keygen to Licenz, including data export, API mapping, and client SDK updates.

Migration Overview

The migration process involves four main steps:

  1. Export data from Keygen - Products, policies, licenses, and users
  2. Transform and import to Licenz - Map Keygen concepts to Licenz
  3. Update client integrations - Switch SDK and API calls
  4. Validate and cutover - Test thoroughly before going live

Plan Your Migration

We recommend running Keygen and Licenz in parallel during migration. This allows you to validate the migration without service interruption.

Concept Mapping

Understanding how Keygen concepts map to Licenz:

Keygen Licenz Notes
Account Organization Top-level container
Product Product Direct mapping
Policy Product Settings Policies are merged into product configuration
License License Direct mapping with enhanced features
Machine Activation Renamed for clarity
User Customer Optional customer association
Entitlement Feature Flag Enhanced with rules engine

Step 1: Export Data from Keygen

Use the Keygen API to export all your data:

#!/bin/bash
# Keygen Data Export Script
# Exports licenses, products, and policies from Keygen

KEYGEN_ACCOUNT_ID="your-account-id"
KEYGEN_TOKEN="your-admin-token"
OUTPUT_DIR="./keygen-export"

mkdir -p "$OUTPUT_DIR"

# Export products
curl -s "https://api.keygen.sh/v1/accounts/$KEYGEN_ACCOUNT_ID/products"   -H "Authorization: Bearer $KEYGEN_TOKEN"   -H "Accept: application/vnd.api+json"   > "$OUTPUT_DIR/products.json"

# Export policies
curl -s "https://api.keygen.sh/v1/accounts/$KEYGEN_ACCOUNT_ID/policies"   -H "Authorization: Bearer $KEYGEN_TOKEN"   -H "Accept: application/vnd.api+json"   > "$OUTPUT_DIR/policies.json"

# Export licenses (paginated)
page=1
while true; do
  response=$(curl -s "https://api.keygen.sh/v1/accounts/$KEYGEN_ACCOUNT_ID/licenses?page[number]=$page&page[size]=100"     -H "Authorization: Bearer $KEYGEN_TOKEN"     -H "Accept: application/vnd.api+json")

  echo "$response" > "$OUTPUT_DIR/licenses_page_$page.json"

  # Check if more pages
  has_next=$(echo "$response" | jq -r '.links.next // empty')
  [ -z "$has_next" ] && break
  ((page++))
done

# Export users
curl -s "https://api.keygen.sh/v1/accounts/$KEYGEN_ACCOUNT_ID/users"   -H "Authorization: Bearer $KEYGEN_TOKEN"   -H "Accept: application/vnd.api+json"   > "$OUTPUT_DIR/users.json"

echo "Export complete: $OUTPUT_DIR"

Verify Export

# Check exported files
ls -la keygen-export/

# Count licenses
cat keygen-export/licenses_page_*.json | jq '.data | length' | paste -sd+ | bc

# Preview products
cat keygen-export/products.json | jq '.data[].attributes.name'

Step 2: Transform and Import

Use the migration script to convert and import your data:

#!/usr/bin/env python3
"""
Keygen to Licenz Migration Script
Converts Keygen export data to Licenz format and imports via API
"""

import json
import requests
import os
from datetime import datetime

LICENZ_API_URL = "https://api.licenz.io/v1"  # or your self-hosted URL
LICENZ_API_KEY = os.environ.get("LICENZ_API_KEY")
EXPORT_DIR = "./keygen-export"

def load_keygen_data(filename):
    with open(f"{EXPORT_DIR}/{filename}") as f:
        data = json.load(f)
    return data.get("data", [])

def map_policy_to_product(policy):
    """Map Keygen policy to Licenz product"""
    return {
        "name": policy["attributes"]["name"],
        "description": policy["attributes"].get("metadata", {}).get("description", ""),
        "metadata": {
            "keygen_policy_id": policy["id"],
            "max_machines": policy["attributes"].get("maxMachines"),
            "max_uses": policy["attributes"].get("maxUses"),
            "duration": policy["attributes"].get("duration"),
        }
    }

def map_license(license_data, policy_map):
    """Map Keygen license to Licenz license"""
    attrs = license_data["attributes"]
    policy_id = license_data["relationships"]["policy"]["data"]["id"]

    # Map license type
    keygen_scheme = attrs.get("scheme", "ED25519_SIGN")
    licenz_type = "perpetual"  # default

    if attrs.get("expiry"):
        licenz_type = "subscription"

    return {
        "key": attrs.get("key"),
        "product_id": policy_map.get(policy_id),
        "type": licenz_type,
        "status": "active" if attrs.get("status") == "ACTIVE" else "suspended",
        "email": attrs.get("metadata", {}).get("email", ""),
        "expires_at": attrs.get("expiry"),
        "max_activations": attrs.get("maxMachines", 1),
        "metadata": {
            "keygen_license_id": license_data["id"],
            "keygen_policy_id": policy_id,
            "original_created_at": attrs.get("created"),
        }
    }

def create_product(product_data):
    """Create product in Licenz"""
    response = requests.post(
        f"{LICENZ_API_URL}/products",
        headers={
            "Authorization": f"Bearer {LICENZ_API_KEY}",
            "Content-Type": "application/json"
        },
        json=product_data
    )
    response.raise_for_status()
    return response.json()["data"]["id"]

def create_license(license_data):
    """Create license in Licenz"""
    response = requests.post(
        f"{LICENZ_API_URL}/licenses",
        headers={
            "Authorization": f"Bearer {LICENZ_API_KEY}",
            "Content-Type": "application/json"
        },
        json=license_data
    )
    response.raise_for_status()
    return response.json()["data"]["id"]

def main():
    print("Starting Keygen to Licenz migration...")

    # Load exported data
    policies = load_keygen_data("policies.json")

    # Create products from policies
    policy_map = {}  # keygen_policy_id -> licenz_product_id

    print(f"Migrating {len(policies)} policies as products...")
    for policy in policies:
        product_data = map_policy_to_product(policy)
        licenz_product_id = create_product(product_data)
        policy_map[policy["id"]] = licenz_product_id
        print(f"  Created product: {product_data['name']} -> {licenz_product_id}")

    # Migrate licenses
    page = 1
    total_licenses = 0

    while os.path.exists(f"{EXPORT_DIR}/licenses_page_{page}.json"):
        licenses = load_keygen_data(f"licenses_page_{page}.json")

        for license_data in licenses:
            try:
                license_mapped = map_license(license_data, policy_map)
                licenz_license_id = create_license(license_mapped)
                total_licenses += 1
                print(f"  Migrated license: {license_mapped['key'][:20]}... -> {licenz_license_id}")
            except Exception as e:
                print(f"  Error migrating license {license_data['id']}: {e}")

        page += 1

    print(f"\nMigration complete!")
    print(f"  Products created: {len(policy_map)}")
    print(f"  Licenses migrated: {total_licenses}")

if __name__ == "__main__":
    main()

Run the Migration

# Set your Licenz API key
export LICENZ_API_KEY="your-api-key"

# Run migration (dry run first)
python3 migrate.py --dry-run

# Run actual migration
python3 migrate.py

# Verify migration
curl -H "Authorization: Bearer $LICENZ_API_KEY" \
  https://api.licenz.io/v1/licenses | jq '.meta.total'

API Endpoint Mapping

Reference for updating API calls:

Keygen Endpoint Licenz Endpoint Notes
POST /licenses/actions/validate-key POST /v1/licenses/validate Direct mapping
GET /licenses/:id GET /v1/licenses/:id Direct mapping
POST /licenses POST /v1/licenses Direct mapping
PATCH /licenses/:id PATCH /v1/licenses/:id Direct mapping
DELETE /licenses/:id DELETE /v1/licenses/:id Direct mapping
POST /machines POST /v1/activations Renamed to activations
DELETE /machines/:id DELETE /v1/activations/:id Renamed
GET /policies N/A Policies merged into products

Step 3: Update Client Integrations

JavaScript/TypeScript

// Before: Keygen SDK
import Keygen from 'keygen-sdk';

const keygen = new Keygen({
  account: 'your-account-id',
});

const validation = await keygen.licenses.validate({
  key: licenseKey,
  fingerprint: machineFingerprint,
});

// After: Licenz SDK
import { Licenz } from 'licenz';

const licenz = new Licenz({
  apiKey: 'your-api-key',
  // For self-hosted:
  // baseUrl: 'https://licenz.yourdomain.com/api/v1'
});

const validation = await licenz.licenses.validate({
  key: licenseKey,
  fingerprint: machineFingerprint,
});

Rust

// Before: Keygen validation (HTTP request)
let response = client
    .post("https://api.keygen.sh/v1/accounts/ACCOUNT/licenses/actions/validate-key")
    .header("Authorization", format!("Bearer {}", api_key))
    .json(&json!({
        "meta": {
            "key": license_key,
            "scope": { "fingerprint": fingerprint }
        }
    }))
    .send()
    .await?;

// After: Licenz (using licenz crate)
use licenz::{Client, LicenseValidation};

let client = Client::new("your-api-key");

let validation = client
    .licenses()
    .validate(license_key)
    .fingerprint(fingerprint)
    .send()
    .await?;

match validation {
    LicenseValidation::Valid(license) => {
        println!("License valid until: {:?}", license.expires_at);
    }
    LicenseValidation::Invalid(reason) => {
        println!("License invalid: {}", reason);
    }
}

Request/Response Mapping

The validation request format is similar but simplified:

// Keygen validation request
{
  "meta": {
    "key": "ABC-123-DEF",
    "scope": {
      "fingerprint": "device-fingerprint"
    }
  }
}

// Licenz validation request
{
  "key": "ABC-123-DEF",
  "fingerprint": "device-fingerprint"
}

Step 4: Validation and Cutover

Pre-Cutover Checklist

  • All products migrated and verified
  • All licenses migrated with correct metadata
  • Test validation with sample licenses
  • Client SDK updated and tested in staging
  • Webhooks reconfigured (if used)
  • Rollback plan documented
  • Support team notified

Validation Script

#!/bin/bash
# Validate migration by testing sample licenses

# Test license validation
curl -X POST https://api.licenz.io/v1/licenses/validate \
  -H "Authorization: Bearer $LICENZ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key": "MIGRATED-LICENSE-KEY"}'

# Compare license counts
KEYGEN_COUNT=$(cat keygen-export/licenses_page_*.json | jq '[.data | length] | add')
LICENZ_COUNT=$(curl -s -H "Authorization: Bearer $LICENZ_API_KEY" \
  https://api.licenz.io/v1/licenses | jq '.meta.total')

echo "Keygen licenses: $KEYGEN_COUNT"
echo "Licenz licenses: $LICENZ_COUNT"

if [ "$KEYGEN_COUNT" -eq "$LICENZ_COUNT" ]; then
  echo "License counts match!"
else
  echo "WARNING: License count mismatch"
fi

Parallel Operation Period

During migration, you can run both systems in parallel:

// Dual-validation during migration period
async function validateLicense(key: string, fingerprint: string) {
  // Try Licenz first
  try {
    const result = await licenz.licenses.validate({ key, fingerprint });
    return result;
  } catch (licenzError) {
    // Fallback to Keygen during migration
    console.warn('Licenz validation failed, falling back to Keygen');
    return await keygen.licenses.validate({ key, fingerprint });
  }
}

Handling Special Cases

Floating Licenses

Keygen floating licenses map to Licenz concurrent activations:

// Keygen floating license policy
{
  "maxMachines": 5,
  "floating": true,
  "floatingInterval": 3600
}

// Licenz equivalent (in product settings)
{
  "max_concurrent_activations": 5,
  "activation_heartbeat_interval": 3600
}

Component Licenses

// Keygen component validation
{
  "meta": {
    "key": "LICENSE-KEY",
    "scope": {
      "fingerprint": "device-fingerprint",
      "components": [
        {"name": "cpu", "fingerprint": "cpu-id"},
        {"name": "motherboard", "fingerprint": "mb-id"}
      ]
    }
  }
}

// Licenz component validation
{
  "key": "LICENSE-KEY",
  "fingerprint": "device-fingerprint",
  "components": {
    "cpu": "cpu-id",
    "motherboard": "mb-id"
  }
}

Post-Migration

Decommission Keygen

After successful migration and validation period:

  1. Remove Keygen SDK from your application
  2. Update documentation to reference Licenz
  3. Cancel Keygen subscription
  4. Archive Keygen export data for records

Optimize for Licenz Features

Take advantage of Licenz-specific features:

  • Offline Activation - Air-gapped validation support
  • Hardware Binding - Enhanced fingerprinting options
  • Rules Engine - Complex license policies with Graxon
  • Self-Hosted - Run on your own infrastructure

Getting Help

If you encounter issues during migration:

Next Steps