Skip to main content

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.

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.

Prerequisites

Before you begin, ensure that you’ve:
  • Obtained a Circle Developer API key from the Circle Console.
  • Completed the 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, 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, iOS SDK, Android SDK, or React Native SDK.
  • Installed the user-controlled wallet server-side SDK in your backend to create the transaction challenge: Node.js or Python.

Steps

1

Acquire a session token

Request a 60-minute session token for the user. The token authorizes the transaction challenge later in the flow.
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;
2

Identify the wallet and token

Get the user’s wallet and confirm it has a balance of the token you want to transfer.
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;
3

Estimate transaction cost (optional)

Estimate gas fees before initiating the transfer to surface them to the user or block low-balance transfers early.
const estimate = await client.estimateTransferFee({
  userToken,
  amount: ["0.01"],
  destinationAddress: "0xEb9614D6d001391e22dDbbEA7571e9823A469c1f",
  tokenId,
  walletId,
});
The response returns fee estimates at low, medium, and high priorities.
4

Initiate the transaction

Create a transaction challenge that the user authorizes in the next step. Pick the tab for the operation you’re performing.
The endpoints below accept either walletId or walletAddress + blockchain to identify the source wallet. Use whichever you have stored for the user.
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;
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 for details on idempotency key usage.
5

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 to customize or replace it.
  • PIN: The user enters their PIN (or uses biometrics) to authorize.
The SDK completes the challenge with Circle.
6

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.
Subscribe to outbound transaction notifications. Circle sends a notification when the transaction state changes.
Webhook notification
{
  "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 Webhook Notifications.
For a full list of transaction states and timing expectations, see Asynchronous States and Statuses.
7

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.

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 or a paymaster to sponsor gas. See 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.