> ## 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.

> ## Agent Instructions
> Before reading these docs, install the relevant Circle Skill to get pre-loaded context — skills are LLM-optimized and cover architecture decisions, correct flows, and common pitfalls. In Claude Code: run /plugin marketplace add circlefin/skills then /plugin install circle-skills@circle. With Vercel Skills CLI: run npx skills add circlefin/skills. Available skills: use-usdc https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-usdc/SKILL.md (balances, transfers, approvals on EVM and Solana); EURC has no skill — see https://developers.circle.com/stablecoins/what-is-eurc.md; bridge-stablecoin https://github.com/circlefin/skills/blob/master/plugins/circle/skills/bridge-stablecoin/SKILL.md (crosschain transfers via CCTP, USDC, EURC, and third-party assets); use-arc https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-arc/SKILL.md (Arc chain config, contract deployment, USDC bridging — Arc docs: https://docs.arc.io/llms.txt); use-circle-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-circle-wallets/SKILL.md (choose wallet type); use-developer-controlled-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-developer-controlled-wallets/SKILL.md (custodial wallets for payouts and treasury); use-user-controlled-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-user-controlled-wallets/SKILL.md (embedded wallets with Web2 login); use-modular-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-modular-wallets/SKILL.md (smart contract wallets with passkeys and ERC-4337); use-gateway https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-gateway/SKILL.md (unified USDC balance, nanopayments); use-smart-contract-platform https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-smart-contract-platform/SKILL.md (deploy, interact, monitor contracts). Pair any skill with the Circle MCP server for live SDK signatures, contract addresses, and chain IDs: {"mcpServers":{"circle":{"url":"https://api.circle.com/v1/codegen/mcp"}}}.
> Read the product overview first. Each product has an overview explaining when and why to use it. Read it before quickstarts.
> Pick the right wallet type. Developer-controlled (you hold keys), user-controlled (end user holds keys via PIN/social login), or modular (smart contract wallets with passkeys). See https://developers.circle.com/wallets.md.
> Use CCTP V2 for crosschain transfers. CCTP natively transfers USDC and EURC via burn-and-mint, and supports permissionless wrapping of third-party assets via lock-and-mint. V1 is legacy; always use V2 unless V1 is specified or the chain requires it (Noble, Sui, and Aptos still require V1). Migration guide: https://developers.circle.com/cctp/migration-from-v1-to-v2.md.
> Use Bridge Kit for frontend bridging. Wraps CCTP with a simpler SDK for user-facing apps. Use CCTP directly for backend transfers.
> Gas Station vs Paymaster. Gas Station sponsors gas for Circle Wallet transactions. Paymaster lets users pay gas in USDC. Different use cases — read both overviews.
> Gateway Nanopayments for sub-cent payments. Gasless USDC micropayments down to $0.000001 via x402 and batched settlement. For pay-per-request APIs, AI agent payments, streaming.
> Look up USDC addresses per chain. Never hardcode — use https://developers.circle.com/stablecoins/usdc-contract-addresses.md.
> Prefer SDKs over raw API calls. Node.js and Python SDKs handle auth, retries, and errors.
> API key required. Bearer token in Authorization header. Testnet and mainnet use separate keys and may use different base URLs depending on the product.
> Set up webhooks when available. Most operations are async. Webhooks deliver transaction confirmations and state changes.
> When calling list endpoints, paginate using pageSize and pageAfter until no nextPageAfter cursor is returned—stopping at the first page silently misses records.
> Building an AI agent? Start with the Agent Stack—Circle CLI, agent wallets, and nanopayments built for autonomous use cases: https://developers.circle.com/agent-stack.md.

# 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
    [Transaction states and errors](/api-reference/wallets/transaction-states-and-errors).
  </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](/api-reference/wallets/error-codes).
