> ## Documentation Index
> Fetch the complete documentation index at: https://docs.privy.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Handle account transfer in your mobile app

> Catch account transfer conflicts on iOS and Android, and let users move a login method from an orphaned account to their currently authenticated account

**When a user tries to link a login method that's already linked to a different Privy user, the
SDK surfaces a transfer conflict instead of failing outright.** Your app can catch this conflict,
confirm the transfer with the user, and re-submit the login method to move it onto the currently
authenticated account.

<Info>
  Programmatic account transfer is currently available on the Privy iOS and Android native SDKs
  only.
</Info>

<Warning>
  **Login method transfer must be enabled in your [dashboard](https://dashboard.privy.io)** under
  **User management > Authentication > Login method transfer** before the SDK will surface transfer
  conflicts. See [Configure account transfer](/recipes/dashboard/account-transfer) for background on
  the underlying flow, including the current limitation that the orphaned account must have only one
  login method.
</Warning>

## How it works

1. Your app calls a `link` (or `linkWithCode`) method for email, SMS, OAuth, SIWE, or SIWS.
2. If that login method is already linked to a **different** Privy user who has no other login
   methods, the call fails with a transfer conflict instead of a generic link error. The conflict
   carries a one-time `nonce` and `ConflictingUserMetadata` describing the other account.
3. Your app shows a confirmation prompt (e.g. "Transfer this email from *display name*?") using
   the metadata.
4. If the user confirms, your app re-proves ownership of the login method (resubmit the OTP code,
   redo the OAuth flow, or re-sign the SIWE/SIWS message) and calls the corresponding
   `transferWithCode`/`transferAccount` method with the `nonce`.
5. On success, the login method moves to the current user, and the orphaned account is deleted.

<Warning>
  Transferring an account only moves the login method itself. It does **not** transfer any embedded
  wallet from the orphaned account — that wallet, if any, remains with the deleted account's assets
  orphaned along with it. Don't let users trigger a transfer without warning them about this if the
  conflicting account might hold funds.
</Warning>

## Catching the transfer conflict

<View title="iOS" icon="swift">
  On iOS, a transfer conflict is thrown as a `PrivyError` with `errorCode`
  `.authenticationFailure(.accountTransferRequired(nonce:conflictingUser:))`:

  ```swift theme={"system"}
  do {
      let user = try await privy.email.linkWithCode(code, sentTo: email)
  } catch let privyError as PrivyError {
      guard case .authenticationFailure(.accountTransferRequired(let nonce, let conflictingUser)) = privyError.errorCode else {
          // Some other link failure — handle normally
          return
      }
      // Show a confirmation prompt using `conflictingUser`, then call
      // privy.email.transferWithCode(nonce:code:sentTo:) if the user confirms.
  }
  ```
</View>

<View title="Android" icon="android">
  On Android, methods return `Result<T>` rather than throwing. Inspect the failure for an
  `AccountTransferRequiredException`:

  ```kotlin theme={"system"}
  val result = privy.email.linkWithCode(code, email)

  result.onFailure { error ->
      if (error is AccountTransferRequiredException) {
          // Show a confirmation prompt using error.conflictingUser, then call
          // privy.email.transferWithCode(error.nonce, code, email) if the user confirms.
      } else {
          // Some other link failure — handle normally
      }
  }
  ```
</View>

### ConflictingUserMetadata

Use these fields only to render the confirmation prompt. They identify a different Privy user, so
avoid logging, persisting, or sending them to analytics.

| Field                      | Description                                                              |
| -------------------------- | ------------------------------------------------------------------------ |
| `displayName`              | A display name for the conflicting account (email address or OAuth name) |
| `embeddedWalletAddress`    | The conflicting user's embedded wallet address, if any                   |
| `farcasterEmbeddedAddress` | The conflicting user's Farcaster-linked embedded wallet address, if any  |
| `oAuthUserInfo`            | OAuth user info fields for the conflicting account, if any               |

## Transferring by login method

Each method requires you to re-prove ownership of the login method before the transfer nonce is
accepted — a stale OTP code or signature won't work, so re-collect it after the user confirms.

### Email

The nonce is tied to a fresh OTP, so send a new code to the same email address before calling
`transferWithCode` — the code from the original `linkWithCode` attempt won't work.

<View title="iOS" icon="swift">
  ```swift theme={"system"}
  try await privy.email.sendCode(to: email)

  // Once the user has entered the new code:
  let user = try await privy.email.transferWithCode(
      nonce: nonce,
      code: code, // the newly-sent OTP, not the original one
      sentTo: email
  )
  ```
</View>

<View title="Android" icon="android">
  ```kotlin theme={"system"}
  privy.email.sendCode(email)

  // Once the user has entered the new code:
  val result = privy.email.transferWithCode(nonce, code, email) // the newly-sent OTP, not the original one
  ```
</View>

### SMS

The nonce is tied to a fresh OTP, so send a new code to the same phone number before calling
`transferWithCode` — the code from the original `linkWithCode` attempt won't work.

<View title="iOS" icon="swift">
  ```swift theme={"system"}
  try await privy.sms.sendCode(to: phoneNumber)

  // Once the user has entered the new code:
  let user = try await privy.sms.transferWithCode(
      nonce: nonce,
      code: code, // the newly-sent OTP, not the original one
      sentTo: phoneNumber
  )
  ```
</View>

<View title="Android" icon="android">
  ```kotlin theme={"system"}
  privy.sms.sendCode(phoneNumber)

  // Once the user has entered the new code:
  val result = privy.sms.transferWithCode(nonce, code, phoneNumber) // the newly-sent OTP, not the original one
  ```
</View>

### OAuth

A fresh OAuth browser flow must run to produce new credentials before the transfer can complete.

<View title="iOS" icon="swift">
  ```swift theme={"system"}
  let user = try await privy.oAuth.transferAccount(
      nonce: nonce,
      with: provider,
      appUrlScheme: appUrlScheme
  )
  ```
</View>

<View title="Android" icon="android">
  ```kotlin theme={"system"}
  val result = privy.oAuth.transferAccount(nonce, oAuthProvider, appUrlScheme)
  ```
</View>

### SIWE (Sign-In with Ethereum)

Generate a fresh SIWE message and signature — don't reuse the one from the original `link` call.

<View title="iOS" icon="swift">
  ```swift theme={"system"}
  let message = try await privy.siwe.generateMessage(params: params)
  let signature = try await wallet.signMessage(message) // your wallet's signing call

  let user = try await privy.siwe.transferAccount(
      nonce: nonce,
      message: message,
      signature: signature,
      params: params
  )
  ```
</View>

<View title="Android" icon="android">
  ```kotlin theme={"system"}
  val message = privy.siwe.generateMessage(params).getOrThrow()
  val signature = wallet.signMessage(message) // your wallet's signing call

  val result = privy.siwe.transferAccount(nonce, message, signature, params)
  ```
</View>

### SIWS (Sign-In with Solana)

Generate a fresh SIWS message and signature — don't reuse the one from the original `link` call.

<View title="iOS" icon="swift">
  ```swift theme={"system"}
  let message = try await privy.siws.generateMessage(params: params)
  let signature = try await wallet.signMessage(message) // your wallet's signing call

  let user = try await privy.siws.transferAccount(
      nonce: nonce,
      message: message,
      signature: signature,
      params: params
  )
  ```
</View>

<View title="Android" icon="android">
  ```kotlin theme={"system"}
  val message = privy.siws.generateMessage(params).getOrThrow()
  val signature = wallet.signMessage(message) // your wallet's signing call

  val result = privy.siws.transferAccount(nonce, message, signature, params)
  ```
</View>

<Tip>
  If the transfer call itself fails (e.g. the nonce expired), surface a normal error and let the
  user restart the link flow from the beginning — nonces are one-time use and tied to the
  credentials submitted alongside them.
</Tip>
