Skip to main content
This guide walks you through migrating existing API keys to Unkey.

Prerequisites

1

Create an Unkey account

Sign up at app.unkey.com
2

Create a workspace and API

Note your workspaceId and apiId:
  • Workspace ID: Settings → General (upper right corner)
Workspace settings page
  • API ID: APIs → Your API (upper right corner)
API page
3

Create a root key

Go to Settings → Root Keys and create one with api.*.create_key and api.*.verify_key permissions.
Root key creation
4

Get a migration ID

Email [email protected] with:
  • Your workspace ID
  • Source system (PostgreSQL, Auth0, etc.)
  • Hash algorithm used
We’ll send you a migrationId.

Supported hash formats

VariantDescription
sha256_base64SHA-256, base64 encoded
sha256_hexSHA-256, hex encoded
bcryptbcrypt hash
Need a different hash format? Contact us — we can add support for your algorithm.

Export your keys

Extract key hashes from your current system. Never include plaintext keys.

Example: PostgreSQL

SELECT 
  key_hash,
  user_id,
  created_at,
  metadata
FROM api_keys
WHERE revoked = false;

Example: MongoDB

db.apiKeys.find(
  { revoked: false },
  { hash: 1, userId: 1, metadata: 1 }
);

Hash your keys (if not already hashed)

If you have plaintext keys, hash them before migration:
const { createHash } = require("node:crypto");

function hashKey(plaintext) {
  return {
    value: createHash("sha256").update(plaintext).digest("base64"),
    variant: "sha256_base64",
  };
}

Migrate to Unkey

Use the migration API to import your keys:
const UNKEY_ROOT_KEY = process.env.UNKEY_ROOT_KEY;
const MIGRATION_ID = "mig_...";  // From support
const API_ID = "api_...";

// Your exported keys
const keysToMigrate = [
  {
    hash: {
      value: "abc123...",  // The hash value
      variant: "sha256_base64",
    },
    externalId: "user_123",  // Link to your user
    meta: {
      plan: "pro",
      migratedFrom: "legacy-system",
    },
    ratelimits: [{
      name: "requests",
      limit: 1000,
      duration: 60000,
    }],
  },
  // ... more keys
];

const response = await fetch("https://api.unkey.com/v2/keys.migrateKeys", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${UNKEY_ROOT_KEY}`,
  },
  body: JSON.stringify({
    migrationId: MIGRATION_ID,
    apiId: API_ID,
    keys: keysToMigrate,
  }),
});

  const result = await response.json();
  if (!response.ok) {
    console.error("Migration failed:", result?.error || `HTTP ${response.status}`);
    return;
  }
  if (!result?.data) {
    console.error("Migration response missing data");
    return;
  }
  console.log("Migrated:", result.data.migrated.length);
  console.log("Failed:", result.data.failed.length);

Response

{
  "meta": { "requestId": "req_..." },
  "data": {
    "migrated": [
      {
        "hash": "abc123...",
        "keyId": "key_..."
      }
    ],
    "failed": [
      {
        "hash": "xyz789...",
        "error": "Key already exists"
      }
    ]
  }
}

Key configuration options

Each key in the migration can include:
FieldDescription
hashRequired. The hash object with value and variant
prefixKey prefix (for display only, not verified)
nameHuman-readable name
externalIdLink to your user/identity
metaCustom JSON metadata
rolesArray of role names
permissionsArray of permission names
ratelimitsRate limit configuration
creditsUsage limit configuration
expiresExpiration timestamp (Unix ms)
enabledWhether key is active (default: true)

Batch migrations

For large migrations, batch your requests:
const BATCH_SIZE = 100;

async function migrateAll(allKeys) {
  const results = { migrated: 0, failed: 0 };
  
  for (let i = 0; i < allKeys.length; i += BATCH_SIZE) {
    const batch = allKeys.slice(i, i + BATCH_SIZE);
    
    const response = await fetch("https://api.unkey.com/v2/keys.migrateKeys", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${UNKEY_ROOT_KEY}`,
      },
      body: JSON.stringify({
        migrationId: MIGRATION_ID,
        apiId: API_ID,
        keys: batch,
      }),
    });
    
    const result = await response.json();
    const batchNum = Math.floor(i / BATCH_SIZE) + 1;
    
    if (!response.ok) {
      console.error(`Batch ${batchNum} failed:`, result?.error || `HTTP ${response.status}`);
      results.failed += batch.length;
      continue;
    }
    if (!result?.data) {
      console.error(`Batch ${batchNum} missing data`);
      results.failed += batch.length;
      continue;
    }
    
    results.migrated += result.data.migrated.length;
    results.failed += result.data.failed.length;
    
    console.log(`Batch ${batchNum}: ${result.data.migrated.length} migrated`);
  }
  
  return results;
}

Update your verification

After migration, update your API to verify keys with Unkey:
// Before: Custom verification
const isValid = await myDatabase.verifyKey(apiKey);

// After: Unkey verification
const response = await fetch("https://api.unkey.com/v2/keys.verifyKey", {
  method: "POST",
  headers: { 
    "Content-Type": "application/json",
    "Authorization": "Bearer " + process.env.UNKEY_ROOT_KEY 
  },
  body: JSON.stringify({ key: apiKey }),
});
const result = await response.json();
if (!response.ok || !result?.data) {
  // Handle error appropriately
  return { valid: false, error: result?.error || "Verification failed" };
}
const isValid = result.data.valid;

Rollback plan

Keep your old verification system running in parallel during migration:
async function verifyKey(apiKey) {
  // Try Unkey first
  const unkeyResult = await verifyWithUnkey(apiKey);
  
  if (unkeyResult.valid) {
    return unkeyResult;
  }
  
  // Fall back to legacy system (temporary)
  const legacyResult = await verifyWithLegacy(apiKey);
  
  if (legacyResult.valid) {
    const maskedKey = apiKey.slice(0, 5) + "..." + apiKey.slice(-5);
    console.log("Key found in legacy, not yet migrated:", maskedKey);
  }
  
  return legacyResult;
}
Remove the fallback once migration is complete and verified.

Need help?

Contact support

We’ll help you plan and execute your migration safely.
Last modified on February 6, 2026