Old Approach (Manual SDK)Previously, @turnkey/sdk-react exposed a TurnkeyProvider and a custom TurnkeyContext;
you manually managed multiple clients (passkeyClient, iframeClient, walletClient, indexedDbClient).
import { TurnkeyProvider } from "@turnkey/sdk-react";
New Approach (Hook-Based: useTurnkey())Use @turnkey/react-wallet-kit’s TurnkeyProvider and useTurnkey hook, which returns state (session, user, wallets, etc.),
high-level helpers (auth, export/import, signing, external wallets), and all TurnkeyClientMethods from @turnkey/core.
import { TurnkeyProvider } from "@turnkey/react-wallet-kit";
Key differences:
Single provider + hook replaces multiple manually managed clients
High-level helpers for auth/export/import/signing/external wallets
Manual client selection/injection has been replaced by a single modal-driven flow (handleLogin) and dedicated OAuth helpers. Stop calling injectCredentialBundle on iframes; the provider manages sessions and completes OAuth automatically.
Old Approach (Manual SDK)Previously, you manually parsed code/state from the URL, exchanged for a token, and injected credentials into an iframe.New Approach (Hook-Based: useTurnkey())Use a dedicated helper to trigger the OAuth flow. The provider completes redirects automatically on load.
const { handleGoogleOauth } = useTurnkey();await handleGoogleOauth({ openInPage: false }); // set true for redirect
Key differences:
No manual URL parsing/token exchange/injection
Single helper per provider; popup or full-page redirect
Redirect completion handled automatically by the provider
The same applies for other OAuth providers such as Apple, Facebook. See Configuring OAuth for more details.
Old Approach (Manual SDK)Previously, passkey login used a read/write session with a freshly generated public key. Passkey signup first created a WebAuthn credential (challenge + attestation) and then created a new sub-organization/user using that credential.
Old Approach (Manual SDK + Server Actions)Previously, OTP auth required:
A server action to initialize OTP (and optionally create a sub-org if none exists) using the parent org API key.
A server action to verify OTP and exchange the verification token for a session (read/write), stamped by the server key.
Client code to fetch the device public key and then call those server actions.
import { useTurnkey } from "@turnkey/sdk-react";import { initEmailAuth, otpLogin } from "./actions";const { indexedDbClient } = useTurnkey();await indexedDbClient?.resetKeyPair();const publicKey = await indexedDbClient!.getPublicKey();// Start OTP on serverconst init = await initEmailAuth({ email, targetPublicKey: publicKey });// Verify on server and exchange for sessionconst session = await otpLogin({ email, publicKey, otpId: init.otpId, otpCode: code,});await indexedDbClient?.loginWithSession(session);
New Approach (Hook-Based: useTurnkey())The provider handles proxy calls; you no longer need to stamp server-side with the parent org key, nor manually exchange verification tokens. Client code initializes OTP and completes it with completeOtp.Initialize (client):
import { useTurnkey } from "@turnkey/react-wallet-kit";const { initOtp } = useTurnkey();const init = await initOtp({ otpType: "OTP_TYPE_EMAIL", contact: email,});// navigate to verification page with init.otpId
Complete (client):
import { useTurnkey } from "@turnkey/react-wallet-kit";import { OtpType } from "@turnkey/sdk-types";const { completeOtp } = useTurnkey();await completeOtp({ otpId, otpCode: code, contact: email, otpType: OtpType.Email, // optional: create sub-org on first login createSubOrgParams: { customWallet, userEmail: email, },});
Key differences:
Server-stamped flows (parent org API key) are no longer required for standard OTP; the provider’s proxy methods handle the secure exchange.
You call proxyInitOtp and completeOtp directly from the client; the SDK manages session creation and storage.
Optional sub-org creation can be passed via createSubOrgParams during completion.
Old Approach (Manual SDK + Server Actions)Previously, wallet authentication required:
Configuring Turnkey with a Wallet interface (e.g., EthereumWallet) and wrapping your app with the provider.
Deriving a public key from the user’s external wallet (for Ethereum this involves a signMessage prompt).
Optionally creating a sub-organization (sign-up) on the server using the parent org API key pair.
Creating a read/write session via loginWithWallet, bound to a browser-managed IndexedDB API key.
import { EthereumWallet } from "@turnkey/wallet-stamper";export const turnkeyConfig = { apiBaseUrl: "https://api.turnkey.com", defaultOrganizationId: process.env.NEXT_PUBLIC_ORGANIZATION_ID!, wallet: new EthereumWallet(),};
New Approach (Hook-Based: useTurnkey())Use the hook-based helpers to trigger wallet authentication flows. The provider abstracts provider discovery, public key derivation, and session creation/storage. A single call will prompt the external wallet for any required signatures and establish a Turnkey session.Key differences:
No manual walletClient.getPublicKey() or message signing to derive a public key
No SessionType or manual IndexedDB session management; the provider manages session lifecycle
One-liners for “Continue with Wallet” (auto sign in or sign up), or explicit Sign Up / Sign In
Works for Ethereum and Solana; pass Chain.Ethereum or Chain.Solana to getWalletProviders and choose a provider
Old Approach (Manual SDK Calls)Previously, wallet listing relied on manually instantiating a Turnkey client and invoking SDK methods to fetch both wallets and their accounts.
New Approach (Hook-Based: useTurnkey())The new method utilizes the useTurnkey React hook, which abstracts data fetching, session management, and provides ready-to-use wallet/account lists and actions.
const { wallets: hookWallets, createWallet, createWalletAccounts, user, session,} = useTurnkey();// Listing wallets is now as simple as using hookWalletsconst wallets = hookWallets ?? [];
Key differences:
No manual wallet/account fetch + merge
Hook provides wallets and accounts with provider-managed session
Old Approach (Manual SDK Calls)Previously, creating wallets and accounts involved calling SDK methods directly (e.g., createWallet, createWalletAccounts) and then refetching wallets to reflect changes.
Additionally, when creating a new Ethereum account you would often compute the next default account at a specific index via a helper (e.g., defaultEthereumAccountAtIndex(index)) and pass that into createWalletAccounts.
import { useTurnkey } from "@turnkey/sdk-react";const { indexedDbClient } = useTurnkey();// Create a new wallet with one Ethereum accountconst { walletId } = await indexedDbClient.createWallet({ walletName: "Demo Wallet", accounts: ["ADDRESS_FORMAT_ETHEREUM"],});// Compute a default Ethereum account at the next index to maintain derivation path orderingconst newAccount = defaultEthereumAccountAtIndex( state.selectedWallet.accounts.length + 1);// Add a new Ethereum account to the created walletconst newAccountAddress = await indexedDbClient.createWalletAccounts({ walletId, accounts: [newAccount],});// Refresh/read wallets to update local stateconst { wallets } = await indexedDbClient.getWallets();
New Approach (Hook-Based: useTurnkey())Use useTurnkey() helpers createWallet and createWalletAccounts.
The provider manages session and state and refreshing wallets; you only invoke the actions.
Old Approach (Manual SDK + iframe)Previously, exporting required manually initializing an export iframe and orchestrating export API calls and decryption within the iframe.
Old ApproachPreviously, transactions were signed using ethers with TurnkeySigner from @turnkey/ethers, wired to the @turnkey/sdk-react client.
You manually constructed the signer/provider and invoked sendTransaction.
import { ethers } from "ethers";import { TurnkeySigner } from "@turnkey/ethers";import { useTurnkey } from "@turnkey/sdk-react";const { turnkey, indexedDbClient } = useTurnkey();const provider = new ethers.JsonRpcProvider(<rpcUrl>);const currentUser = await turnkey.getCurrentUser();const signer = new TurnkeySigner({ client: indexedDbClient, organizationId: currentUser.organization.organizationId, signWith: "0xYourWalletAddress",}).connect(provider);const tx = { to: "0x0000000000000000000000000000000000000000", value: ethers.parseEther("0.001"), type: 2,};await signer.sendTransaction(tx);
New ApproachUse useTurnkey()’s signing helpers:
signTransaction: signs and returns a signature (you broadcast separately).
signAndSendTransaction: signs and broadcasts, returning the on-chain transaction hash.
The provider handles session state and request stamping; you pass a wallet account and an unsigned transaction.
For more details, see: Signing transactions.
Note: rpcUrl is required when using embedded wallets (to broadcast via your chosen RPC).
For external wallets (e.g., MetaMask, Phantom), rpcUrl is not required and will be ignored.Key differences:
No TurnkeySigner or manual provider wiring
Pass an unsigned transaction and account; SDK stamps and sends
Old ApproachMessages were signed via ethers using TurnkeySigner from @turnkey/ethers, with manual signer/provider wiring against the @turnkey/sdk-react client.
import { useTurnkey } from "@turnkey/sdk-react";import { TurnkeySigner } from "@turnkey/ethers";import { ethers } from "ethers";const { turnkey, indexedDbClient } = useTurnkey();const provider = new ethers.JsonRpcProvider(<rpcUrl>);const currentUser = await turnkey.getCurrentUser();const signer = new TurnkeySigner({ client: indexedDbClient, organizationId: currentUser.organization.organizationId, signWith: "0xYourWalletAddress",}).connect(provider);const signature = await signer.signMessage("Hello Turnkey");
New ApproachUse useTurnkey()’s signMessage to sign directly with a selected wallet account. For a modal-driven UX, handleSignMessage opens a confirmation dialog.