Privy enables you to import wallet private keys for use via the Privy API.. This allows you to migrate wallets from external sources to Privy, including from a different wallet provider. Or, your app can enable users to bring an existing external wallet into your application in order to access and manage their assets within your app seamlessly.

Importing a wallet

Only private key import is currently supported. HD wallet import is coming soon.

1. Initialization

Initialize a key import flow by calling the /v1/wallets/import/init endpoint with your wallet address and chain type. See authentication for how to encode your app credentials.
curl --request POST \
  --url https://api.privy.io/v1/wallets/import/init \
  --header 'Authorization: Basic <encoded-app-credentials>' \
  --header 'Content-Type: application/json' \
  --header 'privy-app-id: <privy-app-id>' \
  --data '{
    "address": "<your-wallet-address>",
    "chain_type": "ethereum",
    "entropy_type": "private-key",
    "encryption_type": "HPKE"
  }'
The endpoint will return a public key to encrypt your private key with:
{
  "encryption_public_key": "<base64-encoded-encryption-public-key>",
  "encryption_type": "HPKE"
}

2. Encryption

Encrypt your private key using Hybrid Public Key Encryption (HPKE) with the following configuration:
  • KEM: DHKEM_P256_HKDF_SHA256
  • KDF: HKDF_SHA256
  • AEAD: CHACHA20_POLY1305
  • Mode: BASE
There are two outputs from the encryption step that you’ll provide to Privy during submission:
  • ciphertext: The encrypted private key
  • encapsulated_key: The encapsulated key
Here’s an example of how to encrypt a private key in TypeScript:
import {Chacha20Poly1305} from '@hpke/chacha20poly1305';
import {CipherSuite, DhkemP256HkdfSha256, HkdfSha256} from '@hpke/core';
import {base64} from '@scure/base';

const encryptWithHpke = async ({
  encryptionPublicKey,
  plaintextPrivateKey
}: {
  encryptionPublicKey: Uint8Array;
  plaintextPrivateKey: Uint8Array;
}) => {
  // Deserialize the raw key returned by the `init` request to the Privy API to a public key object
  const suite = new CipherSuite({
    kem: new DhkemP256HkdfSha256(),
    kdf: new HkdfSha256(),
    aead: new Chacha20Poly1305()
  });
  const publicKeyObject = await suite.kem.deserializePublicKey(encryptionPublicKey);

  // Encrypt the plaintext wallet private key
  const sender = await suite.createSenderContext({
    recipientPublicKey: publicKeyObject
  });
  const ciphertext = await sender.seal(plaintextPrivateKey);

  // Return the encapsulated key and ciphertext, converting ArrayBuffer to Uint8Array
  return {
    encapsulatedKey: new Uint8Array(sender.enc),
    ciphertext: new Uint8Array(ciphertext)
  };
};

// The encryption public key is returned by the `init` request to the Privy API
// For example: BPoOQ5k9nRk37v+XQWkmFEjpvW6RS0HQsPF3+IbhgMlc2Qwp/vz7lln1h0MJj/l0crLUhyyjdmC9RnAcpAkUNVQ=
const base64EncodedEncryptionPublicKey = '<encryption-public-key>';
// The wallet private key in hex format. This may have a '0x' prefix or may not depending on the
// provider you're importing from, so make sure to remove it before encrypting if present as shown below.
// For example: 0xe42f4dc0c8396e93891f05c3a34228c395f9b02505ffd0e79d7d2098af8b20a3
const hexEncodedPlaintextPrivateKey = '<your-wallet-private-key>';

const {encapsulatedKey, ciphertext} = await encryptWithHpke({
  encryptionPublicKey: base64.decode(base64EncodedEncryptionPublicKey),
  plaintextPrivateKey: new Uint8Array(
    Buffer.from(
      // Be sure to remove the `0x` prefix if present
      hexEncodedPlaintextPrivateKey.replace(/^0x/, ''),
      'hex'
    )
  )
});

3. Submission

Submit your encrypted private key to the Privy API by calling the /v1/wallets/import/submit endpoint with the rest of your wallet configuration (e.g. an owner, policies, or signers). See creating a wallet for more information on configuration options.
curl --request POST \
  --url https://api.privy.io/v1/wallets/import/submit \
  --header 'Authorization: Basic <encoded-app-credentials>' \
  --header 'Content-Type: application/json' \
  --header 'privy-app-id: <privy-app-id>' \
  --data '{
    "wallet": {
      "address": "<your-wallet-address>",
      "chain_type": "ethereum",
      "entropy_type": "private-key",
      "encryption_type": "HPKE",
      "ciphertext": "<base64-encoded-encrypted-private-key>",
      "encapsulated_key": "<base64-encoded-encapsulated-key>"
    },
    // Optional additional configuration
    "owner": {...},
    "policy_ids": [...],
    "additional_singers": [...]
  }'
The endpoint will return the wallet object of your imported wallet:
{
  "id": "<privy-wallet-id>",
  "address": "<your-wallet-address>",
  "chain_type": "ethereum",
  "policy_ids": [],
  "additional_signers": [],
  "exported_at": null,
  "imported_at": 1753300563195,
  "created_at": 1753300563197,
  "owner_id": null
}

Using imported wallets

Imported wallets function the same way as Privy-generated wallets. See the API reference for Ethereum or Solana for information about how to send transactions and execute other wallet operations.

Architecture

Privy uses end-to-end encryption to securely transmit your private key into the TEE. Importing a wallet takes place over three steps:

Step 1. Initializing key import

In order to end-to-end encrypt an imported wallet directly to the TEE, an asymmetric encryption key is required. Send an initialization request to the Privy API to retrieve a temporary public key used for encrypting private key material before transmission.

Step 2: Encrypting your private key

Encrypt the wallet’s private key using Hybrid Public Key Encryption (HPKE) with the retrieved public key. This ensures the private key remains encrypted during transit and can only be decrypted within the TEE.

Step 3: Submitting the encrypted key

Send the encrypted payload to the Privy API. Within the TEE, the encrypted payload is decrypted and the private key is used to initialize a new wallet. The temporary keypair used for the import process is destroyed to maintain forward secrecy.