Skip to main content
Move homegrown API keys to Unkey by sending the hash of each key, never the key itself. Users keep the keys they already have. After the import, your backend calls keys.verifyKey instead of looking keys up in your own table. After the move, you no longer store plaintext keys or write the hash comparison. If you switch those checks over too, you also stop running your own rate-limit counters and monthly usage counters. You still run your API, unless you deploy it on Compute, and you still return 401 and 429 to your users. Unkey stores a SHA-256 hash of the full key, base64 encoded, and compares that on verification. See How Unkey stores API keys. Agree the hash format with Unkey before the import. The two formats are below.

Get a migration ID

Email support@unkey.com with your workspace ID, the system the keys come from, and how they’re hashed. Support sends a migrationId. Two hash formats work today. Keys hashed any other way, such as bcrypt or a salted hash, can’t be imported from the hash alone. Tell support, and the migration needs another plan.

Hash the keys yourself

For sha256, this is the same function Unkey uses, so a key hashed this way works exactly like one Unkey created.
hash.ts
Never include plaintext keys in the export or the migration request.
Create a root key with the permissions listed on this page under Root Keys in the dashboard sidebar. CLI commands can instead use a session from unkey login with the required permissions. See Permission reference for every permission.
keys.migrateKeys needs api.*.create_key or api.<api_id>.create_key. Verifying afterwards needs the usual api.*.verify_key. See Root key permissions.

Import the hashes

string
required
The identifier from support, 3 to 255 characters. An unknown ID returns HTTP 404 err:unkey:data:migration_not_found.
string
required
The keyspace to import into.
object[]
required
At least one entry. Each has a required hash (3 or more characters, in your migration’s format) and the same optional fields as keys.createKey: name, externalId, meta, roles (up to 100), permissions (up to 1000), expires, enabled (default true), credits, and ratelimits (up to 50). The rules match Creating keys.

Response

The response is HTTP 200 even when some hashes fail. A hash that already exists anywhere in Unkey, not just in your workspace, is skipped and listed under failed. Every other key is created and returned with its new keyId. Store it next to your own record of the key.
Send large migrations in batches and reconcile migrated against your export after each one.

Switch verification over

Point your backend at keys.verifyKey. For the sha256 algorithm nothing else changes. For the prefixed-api-key algorithm, include the migrationId in every verification during the roll-out:
After a key’s first verification with migrationId, it works with or without the field. A key that hasn’t been verified yet still needs it and returns NOT_FOUND without it, so keep sending migrationId until the fallback below goes quiet.

Run both systems during the switch

Verify against Unkey first. If a key comes back NOT_FOUND, check your old system and log it. Remove the fallback when the log goes quiet. Create new keys with keys.createKey so new users are never in the old system.
Last modified on October 11, 2026