This guide walks you through migrating existing API keys to Unkey.
Prerequisites
Create a workspace and API
Note your workspaceId and apiId:
Workspace ID : Settings → General (upper right corner)
API ID : APIs → Your API (upper right corner)
Create a root key
Go to Settings → Root Keys and create one with api.*.create_key and api.*.verify_key permissions.
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.
Variant Description 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:
Field Description 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.