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

# Migrate to V1

> Complete migration guide for existing clients — all breaking changes with before/after examples.

This guide covers every breaking change in the Wallets SDK V1 release. It is organized by audience — client-side (React / React Native, Swift, Kotlin), server-side (Node.js), and REST API — so you can jump to the section that applies to you.

<Note>
  V1 is a major release with breaking changes. Read through the relevant sections before upgrading your packages.
</Note>

## Terminology changes

V1 renames several concepts. Use this table as a find-and-replace checklist across your codebase.

| Old term                                    | New term                         | Notes                                                                                                                                                                                                                                                       |
| ------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `adminSigner`                               | Recovery signer                  | Recovery signers are high-friction (OTP-based). They can also sign transactions as a fallback when no operational signer is available.                                                                                                                      |
| `delegatedSigner`                           | Signer (operational)             | Day-to-day signers: device, passkey, server, external-wallet.                                                                                                                                                                                               |
| `getOrCreateWallet()`                       | `createWallet()` + `getWallet()` | Separate calls. `getWallet()` throws `WalletNotAvailableError` if no wallet exists.                                                                                                                                                                         |
| `addDelegatedSigner()`                      | `addSigner()`                    | Signature also changed — accepts config objects, not just locator strings.                                                                                                                                                                                  |
| `experimental_activity()`                   | `transfers(params)`              | Name and signature changed. `tokens` and `status` params are optional.                                                                                                                                                                                      |
| `experimental_` prefix                      | Removed                          | `experimental_prepareOnly` → `prepareOnly`, `experimental_signer` → `signer`, `experimental_approval` → `approval`, `experimental_nfts()` → `nfts()`, `experimental_transactions()` → `transactions()`, `experimental_transaction(id)` → `transaction(id)`. |
| `customAuth` / `experimental_setCustomAuth` | `setJwt()`                       | `setJwt(jwt)` replaces the old `experimental_setCustomAuth({ jwt, email, externalWalletSigner })`.                                                                                                                                                          |
| `useWalletEmailSigner`                      | `useWalletOtpSigner`             | Now works for both email and phone OTP signers.                                                                                                                                                                                                             |
| `createOnLogin.signer`                      | `createOnLogin.recovery`         | The `signer` field in `createOnLogin` is now `recovery`.                                                                                                                                                                                                    |
| `createOnLogin.delegatedSigners`            | `createOnLogin.signers`          | Array of operational signers to register at wallet creation.                                                                                                                                                                                                |
| `Activity` type                             | `Transfers` type                 | Find-and-replace import.                                                                                                                                                                                                                                    |

## Client-side migration (React / React Native)

### 1. Update packages

Update all `@crossmint` packages to V1 versions:

```bash theme={null}
npm install @crossmint/client-sdk-react-ui@latest @crossmint/wallets-sdk@latest
```

### 2. Update `createOnLogin` in your provider

The `signer` field is now `recovery`, and `delegatedSigners` is now `signers`.

<CodeGroup>
  ```tsx Before theme={null}
  <CrossmintWalletProvider
    createOnLogin={{
      chain: "base-sepolia",
      signer: { type: "email" },
      delegatedSigners: [{ type: "passkey" }],
    }}
  >
    {/* ... */}
  </CrossmintWalletProvider>
  ```

  ```tsx After theme={null}
  <CrossmintWalletProvider
    createOnLogin={{
      chain: "base-sepolia",
      recovery: { type: "email" },
      signers: [{ type: "passkey" }],
    }}
  >
    {/* ... */}
  </CrossmintWalletProvider>
  ```
</CodeGroup>

<Note>
  Device signers are now the default operational signer. If you do not specify `signers`, a device signer is created automatically on EVM chains. Solana does not support device signers yet — the recovery signer is used as a fallback for signing.
</Note>

### 3. Replace `getOrCreateWallet` with `getWallet` + `createWallet`

`getOrCreateWallet` has been removed. Use `getWallet` to retrieve an existing wallet. If it throws `WalletNotAvailableError`, call `createWallet`.

<CodeGroup>
  ```tsx Before theme={null}
  import { useWallet } from "@crossmint/client-sdk-react-ui";

  const { getOrCreateWallet } = useWallet();

  const wallet = await getOrCreateWallet({
      signer: { type: "email", email: "user@example.com" },
  });
  ```

  ```tsx After theme={null}
  import { useWallet } from "@crossmint/client-sdk-react-ui";
  import { WalletNotAvailableError } from "@crossmint/wallets-sdk";

  const { getWallet, createWallet } = useWallet();

  try {
      const wallet = await getWallet({ chain: "base-sepolia" });
  } catch (error) {
      if (error instanceof WalletNotAvailableError) {
          const wallet = await createWallet({
              chain: "base-sepolia",
              recovery: { type: "email", email: "user@example.com" }, // Replace with user's email
          });
      }
  }
  ```
</CodeGroup>

### 4. Replace `customAuth` with `setJwt`

If you use a third-party auth provider (BYOA), replace `experimental_setCustomAuth` with `setJwt`.

<CodeGroup>
  ```tsx Before theme={null}
  const { experimental_setCustomAuth } = useCrossmint();

  useEffect(() => {
      experimental_setCustomAuth({ jwt, email, externalWalletSigner });
  }, [jwt]);
  ```

  ```tsx After theme={null}
  const { setJwt } = useCrossmint();

  useEffect(() => {
      setJwt(jwt);
  }, [jwt]);
  ```
</CodeGroup>

See the [Bring Your Own Auth](/wallets/guides/bring-your-own-auth) guide for complete setup instructions.

### 5. Remove `experimental_` prefixes

All `experimental_` prefixes have been removed from wallet methods and options.

<CodeGroup>
  ```tsx Before theme={null}
  // Methods
  const activity = await wallet.experimental_activity();
  const nfts = await wallet.experimental_nfts();
  const txs = await wallet.experimental_transactions();
  const tx = await wallet.experimental_transaction(txId);

  // Options
  await wallet.send(to, token, amount, {
      experimental_prepareOnly: true,
      experimental_signer: `external-wallet:${address}`,
  });

  await wallet.approve({
      transactionId,
      options: {
          experimental_approval: { signature, signer: locator },
      },
  });
  ```

  ```tsx After theme={null}
  // Methods
  const transfers = await wallet.transfers({ tokens: "usdc", status: "successful" });
  const nfts = await wallet.nfts();
  const txs = await wallet.transactions();
  const tx = await wallet.transaction(txId);

  // Options
  await wallet.send(to, token, amount, {
      prepareOnly: true,
      signer: `external-wallet:${address}`,
  });

  await wallet.approve({
      transactionId,
      options: {
          approval: { signature, signer: locator },
      },
  });
  ```
</CodeGroup>

### 6. Replace `useWalletEmailSigner` with `useWalletOtpSigner`

The `useWalletEmailSigner` hook has been replaced by `useWalletOtpSigner`, which works for both email and phone OTP flows.

<CodeGroup>
  ```tsx Before theme={null}
  import { useWalletEmailSigner } from "@crossmint/client-sdk-react-native-ui";

  const { needsAuth, sendOtp, verifyOtp, reject } = useWalletEmailSigner();
  ```

  ```tsx After theme={null}
  import { useWalletOtpSigner } from "@crossmint/client-sdk-react-native-ui";

  const { needsAuth, sendOtp, verifyOtp, reject } = useWalletOtpSigner();
  ```
</CodeGroup>

### 7. Replace `addDelegatedSigner` with `addSigner`

The method name and signature have changed. `addSigner` now accepts signer config objects in addition to locator strings.

<CodeGroup>
  ```tsx Before theme={null}
  await wallet.addDelegatedSigner({
      signer: "passkey:abc123",
  });
  ```

  ```tsx After theme={null}
  await wallet.addSigner({
      signer: { type: "passkey" },
  });
  ```
</CodeGroup>

### 8. Remove Farcaster and Dynamic login references

Farcaster sign-in and Dynamic wallet login have been removed entirely. If you used either, migrate to an alternative auth method.

* Remove `"farcaster"` from `loginMethods` in `CrossmintAuthProvider`.
* Remove `"web3"` from `loginMethods`.
* Remove any `FarcasterSignIn`, `Web3AuthFlow`, or `DynamicWalletProvider` imports.

### 9. Understand device signers

Device signers are a new signer type backed by the device's hardware security module (Secure Enclave on mobile, IndexedDB on web). They are the default operational signer for EVM wallets — no OTP is required for day-to-day signing.

Key points:

* No code change is needed if you want the default device signer behavior.
* Call `wallet.needsRecovery()` to check if the device signer needs to be re-established (e.g. after the user switches devices).
* Call `wallet.recover()` to trigger recovery using the recovery signer.
* **Solana does not support device signers yet.** The recovery signer is used as a fallback.

### 10. Use `useSigner()` for non-device signers

If you need to sign with a non-device signer (e.g. passkey, external wallet, server), call `wallet.useSigner()` first.

```tsx theme={null}
// Select a passkey signer for subsequent operations
wallet.useSigner({ type: "passkey" });

// Now send will use the passkey signer
await wallet.send(to, token, amount);
```

<Note>
  `useSigner()` only accepts signer config objects — locator strings (e.g. `"external-wallet:0x..."`) are no longer accepted.
</Note>

### 11. Update external wallet signer to use `onSign`

V1 replaces chain-specific signing props (`provider`, `viemAccount`, `onSignTransaction`, `onSignStellarTransaction`) with a single unified `onSign` callback. When **using** an external-wallet signer (via `useSigner`), both `address` and `onSign` are required.

<CodeGroup>
  ```tsx Before theme={null}
  const wallet = await wallets.getOrCreateWallet("<user-email>", {
      chain: "base-sepolia",
      signer: { type: "email", email: "user@example.com" },
      delegatedSigners: [{
          type: "external-wallet",
          address: "0xABC",
          provider: window.ethereum,
          // or: viemAccount, onSignTransaction (Solana), onSignStellarTransaction (Stellar)
      }],
  });
  ```

  ```tsx After theme={null}
  // Register the signer (address only — no onSign needed)
  const wallet = await wallets.createWallet({
      chain: "base-sepolia",
      recovery: { type: "email", email: "user@example.com" }, // Replace with user's email
      signers: [
          { type: "external-wallet", address: "0xABC" },
      ],
  });

  // Use the signer (onSign is required for signing operations)
  wallet.useSigner({
      type: "external-wallet",
      address: "0xABC",
      onSign: async (payload) => {
          return await myWallet.sign(payload);
      },
  });
  ```
</CodeGroup>

<Note>
  **Registration vs. usage**: When registering an external-wallet signer (in `createWallet({ signers: [...] })` or `addSigner()`), only `address` is needed — no `onSign`. The `onSign` callback is only required when actively signing via `useSigner()`.
</Note>

## Client-side migration (Swift)

### 1. Update the SDK version

In Xcode, go to your package dependency for `crossmint-swift-sdk` and update the version requirement:

<CodeGroup>
  ```text Before theme={null}
  Up to Next Major Version: 0.10.2
  ```

  ```text After theme={null}
  Up to Next Major Version: 1.0.0
  ```
</CodeGroup>

### 2. Replace `getOrCreateWallet` with `getWallet` + `createWallet`

`getOrCreateWallet` has been removed. Use `getWallet` to retrieve an existing wallet. If it returns `nil`, call `createWallet`. The `signer` parameter is now named `recovery`.

<CodeGroup>
  ```swift Before theme={null}
  let wallet = try await sdk.crossmintWallets.getOrCreateWallet(
      chain: EVMChain.baseSepolia,
      signer: EVMSigners.email(email)
  )
  ```

  ```swift After theme={null}
  let wallet: Wallet
  if let existing = try await sdk.crossmintWallets.getWallet(chain: EVMChain.baseSepolia) {
      wallet = existing
  } else {
      wallet = try await sdk.crossmintWallets.createWallet(
          chain: EVMChain.baseSepolia,
          recovery: EVMSigners.email(email)
      )
  }
  ```
</CodeGroup>

### 3. Update OTP authentication

The single `otpAuthentication()` method has been replaced with two explicit steps: `sendEmailOtp` and `confirmEmailOtp`.

<CodeGroup>
  ```swift Before theme={null}
  // Send OTP (code: nil triggers the send)
  try await crossmintAuthManager.otpAuthentication(email: email, code: nil)

  // Verify OTP
  try await crossmintAuthManager.otpAuthentication(email: email, code: otpCode)
  ```

  ```swift After theme={null}
  // Step 1: Send OTP
  try await crossmintAuthManager.sendEmailOtp(email: email)

  // Step 2: Verify OTP
  let status = try await crossmintAuthManager.confirmEmailOtp(
      email: email,
      code: otpCode
  )
  if case .authenticationStatus(.authenticated) = status {
      // User is authenticated, proceed to create wallet
  }
  ```
</CodeGroup>

### 4. Understand device signers

Device signers are a new signer type backed by the device's hardware security module (Secure Enclave). They are the default operational signer for EVM wallets — no OTP is required for day-to-day signing.

Key points:

* No code change is needed if you want the default device signer behavior.
* Opt in explicitly via `WalletOptions(deviceSigner: true)` if you need to control it.
* Call `wallet.needsRecovery()` to check if the device signer needs to be re-established (e.g. after the user reinstalls the app or switches devices).
* Call `wallet.recover()` to trigger recovery using the recovery signer.
* **Solana does not support device signers yet.** The recovery signer is used as a fallback.

### 5. Add and manage signers

Signer management is available on `Wallet`.

```swift theme={null}
// Add a signer
try await wallet.addSigner(SignerConfig.passkey)

// Switch to a specific signer for subsequent operations
try await wallet.useSigner(SignerConfig.passkey)

// Check and trigger recovery if needed
let needsRecovery = await wallet.needsRecovery()
if needsRecovery {
    try await wallet.recover()
}
```

### 6. Update `ApiKeySigner` initialization

`ApiKeySigner` no longer provides a default `ApiKeySignerData` — you must pass one explicitly.

<CodeGroup>
  ```swift Before theme={null}
  let signer = ApiKeySigner()
  ```

  ```swift After theme={null}
  let signer = ApiKeySigner(adminSigner: ApiKeySignerData())
  ```
</CodeGroup>

### 7. Replace the deprecated `Signers` enum

The `Signers` enum is deprecated. Use `EVMSigners` or `SolanaSigners` for type-safe chain compatibility.

<CodeGroup>
  ```swift Before theme={null}
  let signer = Signers.email(email)
  ```

  ```swift After theme={null}
  let signer = EVMSigners.email(email)     // for EVM chains
  // or
  let signer = SolanaSigners.email(email) // for Solana
  ```
</CodeGroup>

## Client-side migration (Kotlin)

### 1. Update the SDK version

Update the version in your `libs.versions.toml` or `build.gradle.kts`:

<CodeGroup>
  ```toml Before theme={null}
  [versions]
  crossmint-sdk = "0.0.16" # or your current pre-1.0.0 version
  ```

  ```toml After theme={null}
  [versions]
  crossmint-sdk = "1.0.0"
  ```
</CodeGroup>

### 2. Replace `getOrCreateWallet` with `getWallet` + `createWallet`

`getOrCreateWallet` has been removed. Use `getWallet` to retrieve an existing wallet. If it returns a failure, call `createWallet`. The `signer` parameter is now named `recovery`.

<CodeGroup>
  ```kotlin Before theme={null}
  when (val result = crossmintWallets.getOrCreateWallet(
      chain = EVMChain.BaseSepolia,
      signer = SignerType.Email(email)
  )) {
      is Result.Success -> { val wallet = result.value }
      is Result.Failure -> { /* handle error */ }
  }
  ```

  ```kotlin After theme={null}
  val wallet = when (val getResult = crossmintWallets.getWallet(EVMChain.BaseSepolia)) {
      is Result.Success -> getResult.value
      is Result.Failure -> {
          when (val createResult = crossmintWallets.createWallet(
              chain = EVMChain.BaseSepolia,
              recovery = SignerType.Email(email)
          )) {
              is Result.Success -> createResult.value
              is Result.Failure -> return // handle error
          }
      }
  }
  ```
</CodeGroup>

### 3. Understand device signers

Device signers are a new signer type backed by the device's hardware security module (Android Keystore). They are the default operational signer for EVM wallets — no OTP is required for day-to-day signing.

Key points:

* No code change is needed if you want the default device signer behavior.
* Call `wallet.needsRecovery()` to check if the device signer needs to be re-established (e.g. after the user reinstalls the app or switches devices).
* Call `wallet.recover()` to trigger recovery using the recovery signer.
* **Solana does not support device signers yet.** The recovery signer is used as a fallback.

### 4. Add and manage signers

Signer management is a first-class API on `Wallet`.

```kotlin theme={null}
// Add a signer
wallet.addSigner(DelegatedSigner.Passkey)

// Switch to a specific signer for subsequent operations
wallet.useSigner(DelegatedSigner.Passkey)

// Remove a signer
wallet.removeSigner(DelegatedSigner.Passkey)

// Check and trigger recovery if needed
if (wallet.needsRecovery()) {
    wallet.recover()
}
```

### 5. Use `SignerSelection` in `send` and `approve`

`send()` and `approve()` now accept an optional `signer: SignerSelection` parameter. It defaults to `SignerSelection.Admin` (the recovery signer), so existing call sites without the parameter continue to work. Pass `SignerSelection.Delegated(...)` to sign with an operational signer.

```kotlin theme={null}
when (val sendResult = wallet.send(
    recipient,
    tokenLocator,
    amount,
    signer = SignerSelection.Delegated(delegatedSignerData)
)) {
    is Result.Success -> { /* proceed to approve */ }
    is Result.Failure -> { /* handle error */ }
}
```

### 6. Check balances and list transfers

`balances()`, `fund()`, and `listTransfers()` are available directly on `Wallet`.

```kotlin theme={null}
// Check balances
when (val result = wallet.balances()) {
    is Result.Success -> {
        println("ETH: ${result.value.nativeToken.amount}")
        println("USDC: ${result.value.usdc.amount}")
    }
    is Result.Failure -> { /* handle */ }
}

// List transfers
when (val result = wallet.listTransfers(emptyList())) {
    is Result.Success -> { val transfers = result.value.transfers }
    is Result.Failure -> { /* handle */ }
}
```

## Server-side migration (Node.js)

<Warning>
  **`owner` field behavior:**

  * **Client-side**: `owner` must NOT be provided — it is automatically derived from the JWT. The client-side types (`ClientSideWalletArgsFor`, `ClientSideWalletCreateArgs`) omit it entirely.
  * **Server-side with email/phone signers**: `owner` is **required** — must be manually set.
  * **Server-side with any other signer** (device, passkey, API key, external wallet, server signer): `owner` is **optional**.
</Warning>

### 1. Update packages

```bash theme={null}
npm install @crossmint/wallets-sdk@latest
```

### 2. Replace `getOrCreateWallet` with `createWallet` / `getWallet`

<CodeGroup>
  ```typescript Before theme={null}
  import { CrossmintWallets, createCrossmint } from "@crossmint/wallets-sdk";

  const crossmint = createCrossmint({ apiKey: "<server-api-key>" });
  const wallets = CrossmintWallets.from(crossmint);

  const wallet = await wallets.getOrCreateWallet("<user-email>", {
      chain: "base-sepolia",
      signer: { type: "email", email: "user@example.com" },
  });
  ```

  ```typescript After theme={null}
  import { CrossmintWallets, createCrossmint, WalletNotAvailableError } from "@crossmint/wallets-sdk";

  const USER_EMAIL = "user@example.com"; // Replace with actual user email
  const crossmint = createCrossmint({ apiKey: "<server-api-key>" });
  const wallets = CrossmintWallets.from(crossmint);

  let wallet;
  try {
      wallet = await wallets.getWallet(`email:${USER_EMAIL}`, { chain: "base-sepolia" });
  } catch (error) {
      if (error instanceof WalletNotAvailableError) {
          wallet = await wallets.createWallet({
              chain: "base-sepolia",
              owner: `email:${USER_EMAIL}`,
              recovery: { type: "email", email: USER_EMAIL },
          });
      }
  }
  ```
</CodeGroup>

### 3. Update signer configuration

The old `signer` field (admin signer) is now `recovery`, and `delegatedSigners` is now `signers`.

<CodeGroup>
  ```typescript Before theme={null}
  const wallet = await wallets.getOrCreateWallet("<user-email>", {
      chain: "base-sepolia",
      signer: { type: "email", email: "user@example.com" },
      delegatedSigners: [
          { type: "external-wallet", address: "0x..." },
      ],
  });
  ```

  ```typescript After theme={null}
  const USER_EMAIL = "user@example.com"; // Replace with actual user email
  const wallet = await wallets.createWallet({
      chain: "base-sepolia",
      owner: `email:${USER_EMAIL}`,
      recovery: { type: "email", email: USER_EMAIL },
      signers: [
          { type: "external-wallet", address: "0x..." },
      ],
  });
  ```
</CodeGroup>

### 4. Migrate from API key signer to server signer

If you were using the API key as a signer for server-side operations, migrate to the new server signer. Server signers use deterministic key derivation from a secret you control.

<CodeGroup>
  ```typescript Before theme={null}
  // API key signer was implicit — the server API key was used to sign
  const wallet = await wallets.getOrCreateWallet("<user-email>", {
      chain: "base-sepolia",
      signer: { type: "email", email: "user@example.com" },
  });

  // Transactions were signed with the API key automatically
  await wallet.send(to, token, amount);
  ```

  ```typescript After theme={null}
  import { CrossmintWallets, createCrossmint } from "@crossmint/wallets-sdk";

  const USER_EMAIL = "user@example.com"; // Replace with actual user email
  // SERVER_SIGNER_SECRET: your server signer secret (set in environment variables)
  const crossmint = createCrossmint({ apiKey: "<server-api-key>" });
  const wallets = CrossmintWallets.from(crossmint);

  const wallet = await wallets.createWallet({
      chain: "base-sepolia",
      owner: `email:${USER_EMAIL}`,
      recovery: { type: "email", email: USER_EMAIL },
      signers: [
          { type: "server", secret: process.env.SERVER_SIGNER_SECRET! },
      ],
  });

  // Select the server signer for signing
  wallet.useSigner({ type: "server", secret: process.env.SERVER_SIGNER_SECRET! });

  await wallet.send(to, token, amount);
  ```
</CodeGroup>

<Warning>
  Store your server signer secret securely. The same secret always derives the same key, so losing it means losing signing access. Crossmint never has access to your secret.
</Warning>

### 5. Remove `experimental_` prefixes

Same as client-side — remove all `experimental_` prefixes from method names and option fields.

## REST API migration

The REST API changes in V1 are additive — there are no breaking changes to existing endpoints. The main addition is **server signer support**.

### Switch to the `2025-06-09` API version

Server signers require the `2025-06-09` API version. Update your base URL:

```
https://staging.crossmint.com/api/2025-06-09/wallets/...
```

### Create a wallet with a server signer

Previously, server-managed signing used the `external-wallet` type. V1 introduces a dedicated `server` signer type.

<CodeGroup>
  ```bash Before theme={null}
  curl -X POST https://staging.crossmint.com/api/2025-06-09/wallets \
    -H "Content-Type: application/json" \
    -H "X-API-KEY: YOUR_SERVER_API_KEY" \
    -d '{
      "type": "evm-smart-wallet",
      "config": {
        "adminSigner": {
          "type": "evm-keypair",
          "address": "ADMIN_WALLET_ADDRESS"
        },
        "delegatedSigners": [
          { "type": "external-wallet", "address": "EXTERNAL_WALLET_ADDRESS" }
        ]
      }
    }'
  ```

  ```bash After theme={null}
  curl -X POST https://staging.crossmint.com/api/2025-06-09/wallets \
    -H "Content-Type: application/json" \
    -H "X-API-KEY: YOUR_SERVER_API_KEY" \
    -d '{
      "type": "evm-smart-wallet",
      "config": {
        "adminSigner": {
          "type": "server",
          "address": "SERVER_SIGNER_ADDRESS"
        },
        "delegatedSigners": [
          { "type": "server", "address": "SERVER_SIGNER_ADDRESS" }
        ]
      }
    }'
  ```
</CodeGroup>

<Note>
  The REST API accepts `address` (not `secret`) for server signers. Derive the address from your secret in your backend before passing it to the API.
</Note>

### Submit approvals with a server signer

Use the `server:<address>` locator format when submitting approvals.

<CodeGroup>
  ```bash Before theme={null}
  curl -X POST https://staging.crossmint.com/api/2025-06-09/wallets/<WALLET_LOCATOR>/transactions/<TRANSACTION_ID>/approvals \
    -H "Content-Type: application/json" \
    -H "X-API-KEY: YOUR_SERVER_API_KEY" \
    -d '{
      "approvals": [{
        "signer": "external-wallet:EXTERNAL_WALLET_ADDRESS",
        "signature": "TX_SIGNATURE"
      }]
    }'
  ```

  ```bash After theme={null}
  curl -X POST https://staging.crossmint.com/api/2025-06-09/wallets/<WALLET_LOCATOR>/transactions/<TRANSACTION_ID>/approvals \
    -H "Content-Type: application/json" \
    -H "X-API-KEY: YOUR_SERVER_API_KEY" \
    -d '{
      "approvals": [{
        "signer": "server:SERVER_SIGNER_ADDRESS",
        "signature": "TX_SIGNATURE"
      }]
    }'
  ```
</CodeGroup>

### REST ↔ SDK signer type mapping

Use this table to map between REST API signer types and SDK signer config objects.

| REST API signer type             | REST locator format         | SDK signer config                                |
| -------------------------------- | --------------------------- | ------------------------------------------------ |
| `evm-keypair` / `solana-keypair` | `external-wallet:<address>` | `{ type: "external-wallet", address, provider }` |
| `server`                         | `server:<address>`          | `{ type: "server", secret: "..." }`              |
| `evm-passkey`                    | `passkey:<credentialId>`    | `{ type: "passkey" }`                            |
| N/A (client-only)                | `device:<publicKey>`        | `{ type: "device" }`                             |
| `evm-email-otp`                  | `email:<email>`             | `{ type: "email", email: "..." }`                |
| `evm-phone-otp`                  | `phone:<number>`            | `{ type: "phone", phone: "..." }`                |

## Checklist

Use this checklist to verify your migration is complete:

* [ ] Updated all `@crossmint` packages to V1 versions
* [ ] Replaced `getOrCreateWallet` with `createWallet` / `getWallet`
* [ ] **(Android)** Updated SDK to `1.0.0`
* [ ] **(Android/iOS)** Replaced `getOrCreateWallet` with `createWallet` / `getWallet`, renamed `signer` → `recovery`
* [ ] **(iOS)** Replaced `otpAuthentication` with `sendEmailOtp` + `confirmEmailOtp`
* [ ] **(iOS)** Updated `ApiKeySigner` initialization — no longer has a default parameter
* [ ] **(iOS)** Replaced deprecated `Signers` enum with `EVMSigners` / `SolanaSigners`
* [ ] Updated `createOnLogin`: `signer` → `recovery`, `delegatedSigners` → `signers`
* [ ] Replaced `experimental_setCustomAuth` with `setJwt` (if using BYOA)
* [ ] Removed all `experimental_` prefixes from methods and options
* [ ] Replaced `useWalletEmailSigner` with `useWalletOtpSigner`
* [ ] Replaced `addDelegatedSigner` with `addSigner`
* [ ] Removed Farcaster / Dynamic login references (if applicable)
* [ ] Updated REST API calls to use `server` signer type (if applicable)
* [ ] Searched codebase for old terms: `adminSigner`, `getOrCreateWallet`, `delegatedSigner`, `experimental_`

## Next steps

<CardGroup cols={2}>
  <Card title="Error handling" icon="triangle-exclamation" href="/wallets/guides/error-handling">
    Handle WalletNotAvailableError and other common errors
  </Card>

  <Card title="Add a signer" icon="key" href="/wallets/guides/signers/add-signers">
    Register operational signers on your wallet
  </Card>
</CardGroup>
