Skip to main content
Turnkey’s current email and SMS OTP flows encrypt each OTP attempt before it leaves the client. The client encrypts the code and a session public key to a Turnkey enclave target key. After successful verification, the resulting verification token is bound to the client public key, so an application server cannot read the OTP or use the token to log in or sign up without the matching client private key. This guide is for integrations that use legacy OTP activities or SDK methods that send otpCode in plaintext. New integrations should use the current flow documented in Email auth & recovery and SMS authentication.
Migrate each OTP attempt as a unit. Do not start an attempt with a legacy INIT_OTP activity and finish it with the updated VERIFY_OTP_V2 or OTP_LOGIN_V2 activities. The encryption bundle, verification token, and client key belong to the same attempt and are not interchangeable with the legacy flow.

Migration checklist

Complete these steps in order. Do not deploy an updated client until its policies allow the updated activity types.
  1. Update exact-version policies. See Update policies before deploying.
  2. Upgrade the frontend and backend Turnkey packages together.
  3. Update the OTP flow. Keep each OTP attempt’s bundle, token, and client key together.
  4. Send the OTP code to the backend only in encryptedOtpBundle.
  5. Test email and SMS separately, if your application supports both.
  6. Test an existing-user login and a new-user signup before deployment.
  7. Deploy the updated client and backend together.
  8. After every client has migrated, remove legacy activity types from exact-version policies.

What changed

ACTIVITY_TYPE_CREATE_SUB_ORGANIZATION_V7 accepts both legacy verification tokens and the enclave-issued tokens from VERIFY_OTP_V2. Migrate customers to the encrypted flow while continuing to use V7. ACTIVITY_TYPE_CREATE_SUB_ORGANIZATION_V8 accepts only enclave-issued tokens and is not required for this migration.
The updated verification flow is:
  1. INIT_OTP_V3 returns otpId and otpEncryptionTargetBundle.
  2. The client generates or selects a P-256 key pair.
  3. The client uses encryptOtpCodeToBundle to encrypt the OTP code and public key with otpEncryptionTargetBundle.
  4. VERIFY_OTP_V2 accepts the encrypted bundle and returns a verification token bound to the client public key.
  5. The client signs the login or signup payload with the same private key.
  6. OTP_LOGIN_V2 or the sub-organization creation flow validates the verification token and client signature.
See OTP login flow for the protocol diagram and security properties.

Upgrade the SDKs

The encrypted flow was introduced in the following releases. Use these versions or later, and upgrade related Turnkey packages together so their generated activity types and request shapes remain compatible. Choose the section below that matches your integration.
In @turnkey/react-wallet-kit, @turnkey/react-native-wallet-kit, and @turnkey/core, initOtp now returns an object instead of a plain OTP ID. Keep both values:
Pass the bundle to verifyOtp. The SDK encrypts the OTP code and public key before calling Turnkey:
verifyOtp no longer accepts contact or otpType, and it no longer returns subOrganizationId. Use completeOtp to perform verification plus account lookup and login/signup, or call proxyGetAccount separately after verification.loginWithOtp and signUpWithOtp no longer accept a separate publicKey. They reuse the key bound during verifyOtp and generate the required client signature automatically.

Update policies before deploying

If a policy checks exact activity versions, update it before the new SDKs begin submitting activities. At minimum, review policies that name:
  • ACTIVITY_TYPE_INIT_OTP or ACTIVITY_TYPE_INIT_OTP_V2
  • ACTIVITY_TYPE_VERIFY_OTP
  • ACTIVITY_TYPE_OTP_LOGIN
  • ACTIVITY_TYPE_CREATE_SUB_ORGANIZATION_V7
During the rollout, exact-type policies must allow the legacy activity types and the updated INIT_OTP_V3, VERIFY_OTP_V2, and OTP_LOGIN_V2 activity types. Keep CREATE_SUB_ORGANIZATION_V7 allowed because it accepts both token types. This does not make activity versions interchangeable within one OTP attempt. Prefer activity.kind when the same policy must cover every version of a specific activity:
CREATE_SUB_ORGANIZATION covers every version of sub-organization creation, not only OTP signup. Preserve any existing restrictions on who may create a sub-organization and which parameters they may submit.
For the complete set of SDK breaking changes shipped with this migration, see tkhq/sdk#1250.