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 support@unkey.com 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.