/v2/keys.migrateKeys.
Before they can call that endpoint, someone on our side has to create a
migration for their workspace. This runbook covers how to do that and what to
send back to the customer.
How key migrations work
A migration is a row in thekey_migrations table
(pkg/mysql/schema/key_migrations.sql) with three fields: the customer-facing
id (what they pass as migrationId), the workspace_id it is scoped to, and
the algorithm that describes how their existing keys are hashed.
When the customer calls /v2/keys.migrateKeys
(svc/api/routes/v2_keys_migrate_keys/handler.go), we store their hashes
verbatim and mark each key with pending_migration_id. Hashes that already
exist are returned in the failed array instead of failing the request.
Identities, roles, and permissions referenced by the imported keys are created
on the fly if they do not exist yet.
The algorithm decides what happens at verification time:
sha256: the customer submits the base64 encoded (standard encoding, not URL-safe) SHA-256 hash of the full key. This is exactly Unkey’s native hash format (pkg/hash), so migrated keys verify through the normal/v2/keys.verifyKeylookup with no extra parameters. Hex encoded SHA-256 does not work.github.com/seamapi/prefixed-api-key: for keys in the Seamprefix_shortToken_longTokenformat. The customer submits the hex encoded SHA-256 hash of the long token only. These keys are found during verification only when the customer includesmigrationIdin the/v2/keys.verifyKeyrequest. On the first successful verification we re-hash the key to our native format, set the display prefix, and clearpending_migration_id(internal/services/keys/get_migrated.go).
algorithm enum in
pkg/mysql/schema/key_migrations.sql and a case to the switch in
internal/services/keys/get_migrated.go. Scope that with the team before
promising it to a customer.
What to ask the customer
Before creating anything, you need their workspace ID, what system the keys live in today, the exact hash algorithm and encoding, the key format (prefixes, separators), and a rough key count. The public migration guide asks them to include most of this in their first email. If they still have plaintext keys, steer them tosha256: they hash the keys
themselves and everything works with zero special handling. Only use the
prefixed-api-key algorithm when they exclusively store hashes of a token
segment and cannot re-hash.
Create the migration
Connect to the production MySQL database and insert the migration row. Theid is customer-facing and globally unique; use the mig_<customer>
convention, for example mig_acme:
/v2/keys.migrateKeys and the verification lookup resolve the
migration scoped to the caller’s workspace, so a wrong workspace ID surfaces
to the customer as err:unkey:data:migration_not_found.
Confirm the row:
Reply to the customer
Send them themigrationId, state the exact hash format the migration
expects, and link the endpoint docs. Fill in the placeholders, and for the
sha256 algorithm state the format as “sha256 hashed and base64 encoded”.
migrationId during verification is
deliberate: for sha256 migrations it is redundant (the native lookup finds
the key directly and the parameter is ignored), but for every other algorithm
it is required, and a template that works for both cannot be applied wrong.
After the customer migrates
The endpoint returns HTTP 200 even on partial success. If the customer reports entries in thefailed array, those hashes already exist in the
system, usually from an earlier partial run. Look the hashes up in the keys
table to confirm whether they belong to the same workspace before advising
the customer to skip or clean them up.
Keys imported under a migration keep pending_migration_id set until their
first verification through the migration path. For sha256 migrations the
column stays set (the native lookup never consults it), which is harmless.
