Non-custodial wallets
Your users hold their own keys. The export happens in the user’s browser and the key travels
straight to Turnkey’s enclaves — it never touches your servers.
Custodial wallets
You hold the keys. Export and import both run from a service you control, so how the key
material is handled between the two is yours to decide.
Asset availability
If your provider doesn’t support exporting key material, importing isn’t an option. You’d generate new wallets in Turnkey and transfer the assets across instead — which is an on-chain operation, with new addresses for your users to re-register anywhere they’ve shared them, plus transaction fees.Choosing your structure
Turnkey holds key material in two different shapes, and the choice affects how you should lay out sub-organizations.
Prefer an HD wallet whenever you have a mnemonic. One HD wallet covers unlimited wallet accounts across chains, and wallet accounts are themselves unlimited — they count against neither the 100 HD wallet limit nor the 1,000 private key limit.
Use private keys when all you have is a raw key — which is the common case when migrating from an MPC provider, since there’s no mnemonic behind the key.
How many private keys per sub-organization?Imported private keys live in your organization’s data, which travels with every signing request, so
the more keys a sub-organization holds, the more data moves on each signature — which shows up as
added signing latency.If you’ll be signing with these keys regularly, keep the count small — a handful per
sub-organization, which falls out naturally from creating one per user. If the keys are retained for
recoverability and signing latency isn’t a concern, you can import as many as you want up to the
1,000 per-organization limit.
Non-custodial wallets
Your users hold their own keys, so only they can perform the export. Within your application you invoke the outgoing provider’s user-facing export method, then hand the user off to Turnkey’s import iframe. The import itself always happens with the user present. There is one import per key or seed phrase, and it runs in their browser, so it can’t be batched or performed on their behalf. Each user’s sub-organization has to exist before they can import into it. Whether you can create those ahead of the cutover depends on how your users authenticate: with email or SMS you already hold the identifier, so you can provision in advance, while a passkey or OAuth credential only exists once the user has authenticated for the first time.1
Create the user's sub-organization
Migrating users still need somewhere for the key to land. If you’re using
See Sub-organization customization
for the full set of options, including per-authentication-method configuration.
@turnkey/react-wallet-kit,
the sub-organization is created automatically on first sign-up.No wallet is created unless you ask for one, which suits a migration: the user’s imported wallet
is the only one they end up with. If you do want a fresh wallet alongside it — to give them a
Turnkey-generated address to move assets to, for example — specify it with createSuborgParams:If you’d rather create sub-organizations from your own backend — when you provision ahead of
the user’s first login, for example — call
createSubOrganization from
@turnkey/sdk-server.
The wallet parameter is optional there too.2
Export from the existing provider
This typically takes the form of an export modal that displays the private key or seed phrase in
plaintext to the user. The specifics depend on your provider, but popular choices have methods
designed for the user to copy their key or seed to the clipboard:
- Privy —
exportWallet(React) - Dynamic —
exportPrivateKey(React) - Para —
useExportPrivateKey(React;para.exportPrivateKey()for custom UI). Supports embedded EVM, Cosmos, and Solana wallets. Unclaimed pregenerated and guest-mode wallets cannot export. - MetaMask Embedded Wallets — key export (formerly Web3Auth). Programmatic export via the
private_keyRPC method must first be switched on in the dashboard under Project Settings → Advanced → Key export; manual export through the built-in wallet UI is always available. - Fireblocks — full key takeover (NCW / Embedded Wallets only — see the custodial track for Vault migrations)
- Other — some providers expose key export only through their own dashboard or a support request, which rules out a just-in-time in-app migration. Confirm what’s available before committing to an approach.
Provider capabilities change often, and several have added programmatic export recently. Treat
this list as a starting point and check the provider’s current documentation rather than
assuming an export path does or doesn’t exist.
3
Import into Turnkey
Turnkey maintains an embedded iframe on Show this after the user has copied their key or seed in the previous step.
import.turnkey.com so that mnemonics and keys travel
directly between the user’s browser and Turnkey’s enclaves, never through your infrastructure.With @turnkey/react-wallet-kit this is a single call — the kit hosts the iframe, renders the
modal, and handles the bundle exchange for you:curve and addressFormats determine which addresses the imported key produces — see
address formats and curves for the full list and
the default derivation path for each.For lower-level control, or if you’re not using React, @turnkey/iframe-stamper exposes the same
flow directly — see Import wallets,
advanced iframe operations and
Enclave to end-user secure channels.4
Verify and retire the old wallet
Confirm the imported wallet reports the address the user expects before you disable their old
wallet. Prompt the user to move any remaining assets if the addresses differ.
Full working example
A complete Next.js app covering import and export of wallets, wallet accounts, and private keys
with
@turnkey/react-wallet-kit — including the sub-organization customization shown above.It authenticates through the Auth Proxy, but only the login half depends on that — the import and
export calls are the same either way. See advanced backend authentication
if you use your own backend instead of the Auth Proxy.Custodial wallets
This track covers both embedded wallets you custody on your users’ behalf and company wallets your organization owns and operates. Either way both halves of the migration run from a backend service you control: the shape is the same — export, then import — but you’re running it in bulk, and the key material passes through your script.The key material is in your custody between export and import, so that stretch is yours to
design. Turnkey’s guarantee begins at the encryption step:
@turnkey/crypto verifies the
enclave’s signature on the import bundle before encrypting to it, so a substituted target key is
rejected and plaintext never reaches Turnkey’s servers.How you handle the stretch before that is up to you and your threat model. A straightforward
script that holds keys in memory only, runs on a restricted host, never logs them or writes them
to disk, and is torn down afterwards is enough for many teams. If it isn’t enough for yours, the
decryption and re-encryption can be performed somewhere stronger — an HSM, your own enclave, or
an air-gapped machine (see Running a large migration for the
air-gapped variant).1
Determine what you can actually migrate
Before writing any code, confirm what your current provider will hand you. This determines both
what’s possible and whether addresses are preserved.ed25519 has two conventions, and the two systems use different ones. A 32-byte ed25519 private
key can either be the signing scalar, or be a value that gets hashed to produce the scalar.
RFC 8032 specifies the second: SHA-512 the 32 bytes, clamp the lower half, sign with the result.
Turnkey follows RFC 8032. Threshold signing produces the scalar directly, so that is what an MPC
export hands you, with no earlier value behind it:So the import succeeds and gives you a perfectly valid ed25519 key, at an address unrelated to the
one you had. You also cannot work backwards: finding a value that Turnkey would hash into
MPC platforms such as Fireblocks export vault key material as a pair of extended keys, an ECDSA
xprv and an EdDSA fprv. You can expand either one locally at your recorded derivation paths,
and either way you get a 32-byte private key per address. Your paths are fine and the keys are the
right keys. What differs is what those 32 bytes are taken to mean.secp256k1 has one convention, so nothing can go wrong. The 32 bytes are the private scalar.
Both sides compute the address from them the same way, so imported keys keep their addresses:a means
inverting SHA-512. No curve, key format, or address format option changes this.The word “seed” is overloaded here. The 32-byte value RFC 8032 hashes is per-key, and has
nothing to do with a BIP-39 seed, which is a mnemonic-derived wallet root. Fireblocks exports
neither: an xprv and an fprv are extended keys.This is not specific to Turnkey. Ledger, Phantom and solana-keygen are all RFC 8032-based, so
the same exported scalar cannot be imported into any of them either.What to do. On the ECDSA side, expand the xprv at your recorded derivation paths on your own
infrastructure and import each child key individually. The xprv itself cannot be imported as a
wallet root, since Turnkey wallets are BIP-39-rooted with no representation for an externally
supplied extended key.For EdDSA balances, sweep funds to freshly generated Turnkey addresses, or retain the provider’s
recovery material in cold storage as a backstop for anything later sent to the legacy addresses.2
Export from the existing provider
The specifics depend on the provider, but most offer programmatic export. A mnemonic, or a private
key plus its format, is all that’s required to build an import bundle.
- Privy: Node and API
- Dfns: API
- Fireblocks Direct Custody Vault: the workspace Recovery Kit, decrypted on an offline machine with their key recovery tool
3
Create the sub-organization with your backend API key
Each sub-organization needs a root user carrying the API key your migration script will stamp with.Record the
subOrganizationId and rootUserId for each sub-organization you create. Every import
call needs both, and the userId must be the root user inside that sub-organization — not the
parent-org user running the script.4
Import the keys
Importing is always two activities: an
init call that returns a one-time target encryption key
from the enclave, then the import itself. Encryption happens locally, so plaintext key material
never reaches Turnkey — @turnkey/crypto verifies the enclave’s signature on the import bundle
before encrypting to it, so a substituted target key is rejected.Each import consumes the encryption key issued by its own
init. You cannot initialize once
and reuse the bundle across a batch — budget two activities per key.organizationId matters in the encryption step too — the enclave’s signature on the import bundle
is bound to it.Choose addressFormats carefully. The wrong format produces a valid but different address.
Bitcoin in particular has several mainnet variants, which you can infer from the existing address
prefix: 1… P2PKH, 3… P2SH, bc1q… P2WPKH, bc1p… P2TR. See
address formats and curves for the full list.5
Verify against the source
importPrivateKey and importWallet both return the derived addresses. Compare each against the address you expected
from the old provider and stop on mismatch — for MPC migrations this is the real check that your
derivation paths were correct, and it’s much cheaper to catch per key than in bulk at the end.Once the run completes, enumerate the keys in each sub-organization with getPrivateKeys (or
getWallets) and diff the full address set against your source list. Don’t retire anything on the
old provider’s side until that passes clean.Node example
A runnable reference for both paths using
@turnkey/sdk-server. It’s interactive — strip the
prompts and wrap it in a loop to turn it into a migration runner.Turnkey CLI
tkcli supports the same flows from the command line: turnkey wallets init-import / import
and turnkey private-keys init-import / import.Running a large migration
Make the runner resumable. Importing a key whose address, format and curve already exist in that sub-organization fails with gRPCALREADY_EXISTS (code 6) and a message containing “has already been
imported”. A byte-identical resubmit is idempotent, but a re-run with a fresh timestamp is a new
activity and will hard-fail on anything already imported. Treat that specific error as “already
done”, record each success as it happens keyed on something stable like the derivation path, and a
partial run can pick up where it stopped.
Pace the loop to your rate limit. Rate limits vary by plan, and an
unthrottled loop will exceed the lower tiers — add a delay rather than running flat out. Running
imports concurrently won’t speed things up either, since limits are enforced per organization.
Air-gapped key expansion. If key material must be expanded on an offline machine, the import splits
into three passes instead of two: run the init calls online and collect the import bundles, take them
offline to derive and encrypt, then bring the encrypted bundles back to submit. Outstanding encryption
keys have no expiry and are cleared only when their matching import consumes them, so there’s no time
pressure on the offline leg and no need to work in small batches.