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

# How-to: Transfer tokens

> Send tokens or execute a smart contract from a user-controlled wallet, with the user authorizing the transaction through the SDK.

Send tokens from a user-controlled wallet to any blockchain address, or execute
a smart contract from the wallet. The transaction is broadcast onchain and costs
gas. To have the user sign an offchain message instead, use
[Request a signature](/wallets/user-controlled/request-a-signature).

## Prerequisites

Before you begin, ensure that you've:

* Obtained a Circle Developer API key from the
  [Circle Console](https://console.circle.com/).
* Completed the
  [Build a wallet app](/wallets/user-controlled/build-a-wallet-app) tutorial,
  which sets up a user-controlled wallet and stores the user's `userId`.
* Funded the user's wallet with testnet USDC from the
  [Circle Faucet](https://faucet.circle.com/), or otherwise have a non-zero
  token balance to transfer.
* Integrated a user-controlled wallet client-side SDK in your app to walk the
  user through authorizing the transaction challenge:
  [Web SDK](/sdks/user-controlled/web-sdk),
  [iOS SDK](/sdks/user-controlled/ios-sdk),
  [Android SDK](/sdks/user-controlled/android-sdk), or
  [React Native SDK](/sdks/user-controlled/react-native-sdk).
* Installed the user-controlled wallet server-side SDK in your backend to create
  the transaction challenge: [Node.js](/sdks/user-controlled-wallets-nodejs-sdk)
  or [Python](/sdks/user-controlled-wallets-python-sdk).

## Steps

<Steps>
  <Step title="Acquire a session token">
    Request a 60-minute session token for the user. The token authorizes the
    transaction challenge later in the flow.

    <CodeGroup>
      ```ts Node.js SDK theme={null}
      import { initiateUserControlledWalletsClient } from "@circle-fin/user-controlled-wallets";

      const client = initiateUserControlledWalletsClient({
        apiKey: process.env.CIRCLE_API_KEY!,
      });

      const response = await client.createUserToken({
        userId: "2f1dcb5e-312a-4b15-8240-abeffc0e3463",
      });

      const userToken: string = response.data!.userToken;
      const encryptionKey: string = response.data!.encryptionKey;
      ```

      ```python Python SDK theme={null}
      from circle.web3 import user_controlled_wallets, utils

      client = utils.init_user_controlled_wallets_client(api_key="<CIRCLE_API_KEY>")
      api_instance = user_controlled_wallets.UsersAndPinsApi(client)

      request = user_controlled_wallets.GenerateUserTokenRequest.from_dict(
          {"userId": "2f1dcb5e-312a-4b15-8240-abeffc0e3463"}
      )
      response = api_instance.get_user_token(request)

      user_token = response.data.user_token
      encryption_key = response.data.encryption_key
      ```
    </CodeGroup>
  </Step>

  <Step title="Identify the wallet and token">
    Get the user's wallet and confirm it has a balance of the token you want to
    transfer.

    <CodeGroup>
      ```ts Node.js SDK theme={null}
      const wallets = await client.listWallets({
        userToken,
        pageSize: 10,
      });

      const walletId: string = wallets.data!.wallets![0].id;

      const balances = await client.getWalletTokenBalance({
        userToken,
        walletId,
      });

      const tokenId: string = balances.data!.tokenBalances![0].token!.id;
      ```

      ```python Python SDK theme={null}
      wallets_api = user_controlled_wallets.WalletsApi(client)

      wallets = wallets_api.list_wallets(user_token)
      wallet_id = wallets.data.wallets[0].id

      balances = wallets_api.list_wallet_balance(user_token, wallet_id)
      token_id = balances.data.token_balances[0].token.id
      ```
    </CodeGroup>
  </Step>

  <Step title="Estimate transaction cost (optional)">
    Estimate gas fees before initiating the transfer to surface them to the user or
    block low-balance transfers early.

    <CodeGroup>
      ```ts Node.js SDK theme={null}
      const estimate = await client.estimateTransferFee({
        userToken,
        amount: ["0.01"],
        destinationAddress: "0xEb9614D6d001391e22dDbbEA7571e9823A469c1f",
        tokenId,
        walletId,
      });
      ```

      ```python Python SDK theme={null}
      transactions_api = user_controlled_wallets.TransactionsApi(client)

      request = user_controlled_wallets.EstimateTransferTransactionFeeRequest.from_dict({
          "amounts": ["0.01"],
          "destinationAddress": "0xEb9614D6d001391e22dDbbEA7571e9823A469c1f",
          "tokenId": token_id,
          "walletId": wallet_id,
      })
      estimate = transactions_api.create_transfer_estimate_fee(user_token, request)
      ```
    </CodeGroup>

    The response returns fee estimates at low, medium, and high priorities.
  </Step>

  <Step title="Initiate the transaction">
    Create a transaction challenge that the user authorizes in the next step. Pick
    the tab for the operation you're performing.

    <Tip>
      The endpoints below accept either `walletId` or `walletAddress` + `blockchain`
      to identify the source wallet. Use whichever you have stored for the user.
    </Tip>

    <Tabs>
      <Tab title="Transfer tokens">
        <CodeGroup>
          ```ts Node.js SDK theme={null}
          const transfer = await client.createTransaction({
            userToken,
            walletId,
            tokenId,
            destinationAddress: "0xEb9614D6d001391e22dDbbEA7571e9823A469c1f",
            amounts: ["0.01"],
            fee: {
              type: "level",
              config: { feeLevel: "MEDIUM" },
            },
            idempotencyKey: crypto.randomUUID(),
          });

          const challengeId: string = transfer.data!.challengeId;
          ```

          ```python Python SDK theme={null}
          request = user_controlled_wallets.CreateTransferTransactionForEndUserRequest.from_dict({
              "idempotencyKey": str(uuid.uuid4()),
              "walletId": wallet_id,
              "tokenId": token_id,
              "destinationAddress": "0xEb9614D6d001391e22dDbbEA7571e9823A469c1f",
              "amounts": ["0.01"],
              "fee": {"type": "level", "config": {"feeLevel": "MEDIUM"}},
          })
          transfer = transactions_api.create_transaction(user_token, request)

          challenge_id = transfer.data.challenge_id
          ```
        </CodeGroup>
      </Tab>

      <Tab title="Execute a contract">
        <CodeGroup>
          ```ts Node.js SDK theme={null}
          const execution = await client.createContractExecutionTransaction({
            userToken,
            walletId,
            contractAddress: "0xEb9614D6d001391e22dDbbEA7571e9823A469c1f",
            abiFunctionSignature: "transfer(address,uint256)",
            abiParameters: ["0xeb9614d6d001391e22ddbbea7571e9823a469c1f", "1000000"],
            fee: {
              type: "level",
              config: { feeLevel: "MEDIUM" },
            },
            idempotencyKey: crypto.randomUUID(),
          });

          const challengeId: string = execution.data!.challengeId;
          ```

          ```python Python SDK theme={null}
          request = user_controlled_wallets.CreateContractExecutionTransactionForEndUserRequest.from_dict({
              "idempotencyKey": str(uuid.uuid4()),
              "walletId": wallet_id,
              "contractAddress": "0xEb9614D6d001391e22dDbbEA7571e9823A469c1f",
              "abiFunctionSignature": "transfer(address,uint256)",
              "abiParameters": ["0xeb9614d6d001391e22ddbbea7571e9823a469c1f", "1000000"],
              "fee": {"type": "level", "config": {"feeLevel": "MEDIUM"}},
          })
          execution = transactions_api.create_contract_execution_transaction(user_token, request)

          challenge_id = execution.data.challenge_id
          ```
        </CodeGroup>
      </Tab>
    </Tabs>

    Include an `idempotencyKey` (a UUID) on every transaction. If the request fails
    or times out, retrying with the same key prevents duplicate transactions. See
    [Idempotent requests](/api-reference/idempotent-requests) for details on
    idempotency key usage.
  </Step>

  <Step title="Have the user authorize the transaction">
    Pass the `userToken`, `encryptionKey`, and `challengeId` to your client-side
    SDK. The SDK presents the transaction details and the appropriate authorization
    UI for the user's authentication method:

    * **Social login or email OTP:** Circle displays a confirmation UI by default.
      See [Confirmation UIs](/sdks/user-controlled/confirmation-uis) to customize or
      replace it.
    * **PIN:** The user enters their PIN (or uses biometrics) to authorize.

    The SDK completes the challenge with Circle.
  </Step>

  <Step title="Check transaction status">
    Once the user authorizes the challenge, Circle submits the transaction onchain.
    Use webhooks (push) or polling (pull) to detect when the transaction reaches a
    terminal state: `COMPLETE`, `FAILED`, or `CANCELLED`.

    <Tabs>
      <Tab title="Webhook">
        Subscribe to outbound transaction notifications. Circle sends a notification
        when the transaction state changes.

        ```json Webhook notification theme={null}
        {
          "subscriptionId": "d4c07d5f-f05f-4fe4-853d-4dd434806dfb",
          "notificationId": "acab8c14-92ae-481a-8335-6eb5271da014",
          "notificationType": "transactions.outbound",
          "notification": {
            "id": "ad3f40ae-9c0e-52cf-816f-91838850572a",
            "blockchain": "MATIC-AMOY",
            "tokenId": "36b6931a-873a-56a8-8a27-b706b17104ee",
            "walletId": "01899cf2-d415-7052-a207-f9862157e546",
            "sourceAddress": "0x7b777eb80e82f73f118378b15509cb48cd2c2ac3",
            "destinationAddress": "0x6e5eaf34c73d1cd0be4e24f923b97cf38e10d1f3",
            "transactionType": "OUTBOUND",
            "custodyType": "ENDUSER",
            "state": "COMPLETE",
            "amounts": ["0.01"],
            "txHash": "0x535ff240984f54e755d67cdc9c79c88768fe5997955f09f3a66b4d1126810900",
            "networkFee": "0.07037500047405219",
            "operation": "TRANSFER",
            "userId": "c266945c-f440-4537-85cf-a16b6e33b0cc",
            "createDate": "2023-10-11T21:08:13Z",
            "updateDate": "2023-10-11T21:08:37Z"
          },
          "timestamp": "2023-10-11T21:08:13Z",
          "version": 2
        }
        ```

        For webhook setup, see [Webhooks](/api-reference/webhooks).
      </Tab>

      <Tab title="Polling">
        Poll the transactions list (or `getTransaction` if you have the transaction ID)
        until the transaction reaches a terminal state.

        <CodeGroup>
          ```ts Node.js SDK theme={null}
          async function pollTransaction(
            walletId: string,
            userToken: string,
            createdAfter: string,
          ) {
            const TERMINAL = new Set(["COMPLETE", "FAILED", "CANCELLED"]);
            for (let attempt = 0; attempt < 30; attempt++) {
              const response = await client.listTransactions({ userToken });
              const match = response.data!.transactions!.find(
                (t) => t.walletId === walletId && t.createDate >= createdAfter,
              );
              if (match && TERMINAL.has(match.state!)) return match;
              await new Promise((resolve) => setTimeout(resolve, 2000));
            }
            throw new Error("Transaction polling timed out");
          }
          ```

          ```python Python SDK theme={null}
          import time

          def poll_transaction(wallet_id: str, user_token: str, created_after: str):
              terminal = {"COMPLETE", "FAILED", "CANCELLED"}
              for _ in range(30):
                  response = transactions_api.list_transactions(user_token)
                  match = next(
                      (
                          t for t in response.data.transactions
                          if t.wallet_id == wallet_id and t.create_date >= created_after
                      ),
                      None,
                  )
                  if match and match.state in terminal:
                      return match
                  time.sleep(2)
              raise TimeoutError("Transaction polling timed out")
          ```
        </CodeGroup>
      </Tab>
    </Tabs>

    For a full list of transaction states and timing expectations, see
    [Asynchronous States and Statuses](/wallets/asynchronous-states-and-statuses).
  </Step>

  <Step title="Confirm tokens were received">
    To confirm tokens were credited to a destination wallet (for testing or to
    trigger downstream logic), check the destination wallet's balance the same way
    you did in Step 2. If you control both wallets in your app, you can also rely on
    the inbound `transactions.inbound` webhook to confirm receipt.
  </Step>
</Steps>

## Error handling

Handle these common failure cases when integrating token transfers and contract
execution:

* **Expired session token (error code `155104`):** The `userToken` from Step 1
  expires after 60 minutes. If you get this error, request a new session token
  and retry.
* **Insufficient balance:** The wallet must hold at least the transfer amount
  plus gas. Validate balance and estimated fees (Step 3) before initiating the
  challenge.
* **Insufficient gas:** EOA wallets must hold native tokens for gas. SCA wallets
  can use [Gas Station](/wallets/gas-station) or a paymaster to sponsor gas. See
  [Gas fees](/wallets/gas-fees) for details.
* **Invalid destination address:** Verify the address format matches the
  wallet's blockchain (hex with checksum for EVM, base58 for Solana, and so on).
* **User declines or fails to authorize:** If the user cancels the challenge or
  enters an incorrect PIN, the transaction never broadcasts. Surface the
  cancellation in your UI and let them retry.
* **Idempotency conflicts:** Reusing an `idempotencyKey` from a successful
  request returns the original transaction without creating a new one. Reusing a
  key from a failed request also returns the original failure. Generate a fresh
  key when retrying after a permanent error.

For a complete error code reference, see
[Wallets error codes](/wallets/error-codes).
