We refreshed our doc site!
Bookmarked links may have changed
Read release notesThis guide outlines initiating a currency transfer from a previously created user-controlled wallet. If you have not yet created a user-controlled wallet, go to this guide. If you do not have any tokens in your wallet, go to the inbound transfer guide.
Note that if you’re building with Wallet Signing Service, you must manage the transaction broadcasting and indexing with your blockchain infrastructure.
The following steps utilize Circle's sample applications in combination with API requests that can be done via Circle's API references or cURL requests. cURL request will be provided inline, while API references will be linked from the API endpoint code text. You can find instructions on using it in the testing via the reference pages guide.
Once you have one of the web, iOS, or Android sample applications set up locally, you will then:
GET /config/entity
and copy the App ID from the response body.You will start by making a request to POST /users/token
using a previously created userId. The userToken
is a 60-minute session token to initiate requests requiring a user challenge (PIN code entry). After 60 minutes, the session expires, and a new userToken
must be generated via the same endpoint.
From this response, you will acquire the encryptionKey
and userToken
which you should provide in the respective sample app fields. Additionally, you will use the userToken
in Step 3.
// Import and configure the user-controlled wallet SDK
const { initiateUserControlledWalletsClient } = require('@circle-fin/user-controlled-wallets');
const circleUserSdk = initiateUserControlledWalletsClient({
apiKey: '<API_KEY>'
});
const response = await circleUserSdk.createUserToken({
userId: '2f1dcb5e-312a-4b15-8240-abeffc0e3463'
});
{
"data": {
"userToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCC9.eyJkZXZlbG9wZXJFbnRpdHlFbnZpcm9ubWVudCI6IlRFU1QiLCJlbnRpdHlJZCI6IjRlMDdhOGM5LTIxOTAtNDVlNC1hNjc0LWQyMGFkNjg4MWI3YyIsImV4cCI6MTY5MDU1MjcwNywiaWF0IjoxNjkwNTQ5MTA3LCJpbnRlcm5hbFVzZXJJZCI6ImQ2ZjkzODliLWQ5MzUtNWFlYy1iOTVhLWNjNTk1NjA2YWM5NiIsImlzcyI6Imh0dHBzOi8vcHJvZ3JhbW1hYmxlLXdhbGxldC5jaXJjbGUuY29tIiwianRpIjoiMmE0YmJlMzAtZTdkZi00YmM2LThiODMtNTk0NGUyMzE2ODlkIiwic3ViIjoiZXh0X3VzZXJfaWRfOSJ9.dhfByhxZFbJx0XWlzxneadT4RQWdnxLu3FSN9ln65hCDOfavaTL1sc4h-jUR8i4zMmfdURw3FFcQIdSbm-BUg6M7FP_fp-cs9xBbNmRZa31gMd1aKdcajJ9SvlVrfUowYfGXM3VcNF8rtTFtW-gk1-KzU4u10U35XXbbMcW1moxE0Rqx_fKotDgk2VdITuuds5d5TiQzAXECqeCOCtNoDKktMkglltbnLxOaRl2ReZjGt-ctD2V0DbYNO4T_ndPSUDI6qD7dXQRed5uDcezJYoha3Qj3tFGBglEnox2Y6DWTbllqjwmfTGrU8Pr0yz4jQz7suGwmiCzHPxcpYxMzYQ",
"encryptionKey": "Tlcyxz7Ts9ztRLQq5+pic0MIETblYimOo2d7idV/UFM="
}
}
Before making an outbound transfer, you must gather the token's ID and ensure you are holding a token balance. To do this, make a request to GET /wallets
passing in the wallets userToken
to get the walletId
.
const response = await circleUserSdk.listWallets({
userToken: '<USER_TOKEN>',
pageSize: 10
});
{
"data": {
"wallets": [
{
"id": "01899cf2-d415-7052-a207-f9862157e546",
"state": "LIVE",
"walletSetId": "01899cf2-d407-7f89-b4d9-84d63573f138",
"custodyType": "ENDUSER",
"userId": "2f1dcb5e-312a-4b15-8240-abeffc0e3463",
"address": "0x075e62c80e55d024cfd8fd4e3d1184834461db57",
"addressIndex": 0,
"blockchain": "MATIC-AMOY",
"accountType": "SCA",
"updateDate": "2023-07-28T14:41:47Z",
"createDate": "2023-07-28T14:41:47Z"
}
]
}
}
Next, you will make a request to GET /wallet/{id}/balances
to check the balance of tokens and acquire the tokenId
you intend to transfer. The token ID will be used in the following steps.
const response = await circleUserSdk.getWalletTokenBalance({
userToken: '<USER_TOKEN>',
walletId: '<WALLET_ID>'
});
{
"data": {
"tokenBalances": [
{
"token": {
"id": "36b6931a-873a-56a8-8a27-b706b17104ee",
"blockchain": "MATIC-AMOY",
"tokenAddress": "0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582",
"standard": "ERC20",
"name": "USDC",
"symbol": "USDC",
"decimals": 6,
"isNative": false,
"updateDate": "2024-03-27T17:55:12Z",
"createDate": "2024-03-27T17:55:12Z"
},
"amount": "10",
"updateDate": "2024-04-16T15:52:23Z"
}
]
}
}
To estimate the fees for the transaction to transfer tokens, make a request to POST transactions/transfer/estimateFee
.
const response = await circleUserSdk.estimateTransferFee({
userToken: '<USER_TOKEN>',
amount: ['.01'],
destinationAddress: '0xEb9614D6d001391e22dDbbEA7571e9823A469c1f',
tokenId: '36b6931a-873a-56a8-8a27-b706b17104ee',
walletId: '01899cf2-d415-7052-a207-f9862157e546'
});
{
"data": {
"low": {
"gasLimit": "21000",
"baseFee": "2.456220277",
"priorityFee": "1.022783914",
"maxFee": "5.935224468"
},
"medium": {
"gasLimit": "21000",
"baseFee": "2.456220277",
"priorityFee": "2.655282857",
"maxFee": "7.567723411"
},
"high": {
"gasLimit": "21000",
"baseFee": "2.456220277",
"priorityFee": "15.986229693",
"maxFee": "20.898670247"
}
}
}
Make a request to POST /user/transactions/transfer
to initiate a blockchain transfer from a specifiedwalletId
to a blockchain addressdestinationAddress
. This call returns a challengeId
, used within the sample app, that prompts users to enter their PIN code to authorize the transfer.
If you do not have a wallet to use as a destination for the transfer, you can create another User-Controlled Wallet by stepping through create your first wallet or send funds to any other blockchain wallet such as Metamask.
const response = await circleUserSdk.createTransaction({
userToken: '<USER_TOKEN>',
amounts: ['.01'],
destinationAddress: '0x6E5eAf34c73D1CD0be4e24f923b97CF38e10d1f3',
tokenId: '36b6931a-873a-56a8-8a27-b706b17104ee',
walletId: '01899cf2-d415-7052-a207-f9862157e546',
fee: {
type: 'level',
config: {
feeLevel: 'MEDIUM'
}
}
});
{
"data": {
"challengeId": "0d1b5f41-1381-50af-983b-f54691415158"
}
}
Using the sample application, enter the userToken
and secretKey
returned from Step 2. Also, enter the challengeId
returned from Step 5.
At this point, you should be ready to execute your first transfer through the sample app. Click Execute in the sample app to continue.
The sample application takes you through the authentication process, which includes the user entering their PIN code to authorize the transfer.
As the transfer state
changes and ultimately completes, Circle sends notifications to a subscribed endpoint. You can find a list of all possible states in the Asynchronous States and Statuses guide. The Webhook notification will be similar to the one below.
{
"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"
],
"nfts": null,
"txHash": "0x535ff240984f54e755d67cdc9c79c88768fe5997955f09f3a66b4d1126810900",
"blockHash": "0xa4c5c79500240f3ae3f4e5c5f641198b7c698d83b7539ac4e8cf2d3f5f49bdfd",
"blockHeight": 41100000,
"networkFee": "0.07037500047405219",
"firstConfirmDate": "2023-10-11T21:08:28Z",
"operation": "TRANSFER",
"userId": "c266945c-f440-4537-85cf-a16b6e33b0cc",
"abiParameters": null,
"createDate": "2023-10-11T21:08:13Z",
"updateDate": "2023-10-11T21:08:37Z"
},
"timestamp": "2023-10-11T21:08:13Z",
"version": 2
}
Alternatively, you can poll GET /transactions
using the userId
or userToken
associated with your user.
const response = await circleUserSdk.listTransactions({
userToken: '<USER_TOKEN>'
});
{
"data": {
"transactions": [
{
"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"
],
"nfts": null,
"txHash": "0x535ff240984f54e755d67cdc9c79c88768fe5997955f09f3a66b4d1126810900",
"blockHash": "0xa4c5c79500240f3ae3f4e5c5f641198b7c698d83b7539ac4e8cf2d3f5f49bdfd",
"blockHeight": 41100000,
"networkFee": "0.07037500047405219",
"firstConfirmDate": "2023-10-11T21:08:28Z",
"operation": "TRANSFER",
"userId": "c266945c-f440-4537-85cf-a16b6e33b0cc",
"abiParameters": null,
"createDate": "2023-10-11T21:08:13Z",
"updateDate": "2023-10-11T21:08:37Z"
},
{
"id": "81cf790a-ed95-5d41-b7bd-c4e15390eef6",
"blockchain": "MATIC-AMOY",
"tokenId": "36b6931a-873a-56a8-8a27-b706b17104ee",
"walletId": "01899cf2-d415-7052-a207-f9862157e546",
"sourceAddress": "0x48520ff9b32d8b5bf87abf789ea7b3c394c95ebe",
"destinationAddress": "0x7b777eb80e82f73f118378b15509cb48cd2c2ac3",
"transactionType": "INBOUND",
"custodyType": "ENDUSER",
"state": "COMPLETE",
"amounts": [
"10"
],
"nfts": null,
"txHash": "0x5121f9efec29d4d661ffb0b777727d1f5ba7b5bc286ac4891c82f7b1b80a9485",
"blockHash": "0xba7984dbe7423827b5fd175a636552ae85401c3f2a0c5cdda934a37d6652ac49",
"blockHeight": 41098635,
"networkFee": "0.001911870000955935",
"firstConfirmDate": "2023-10-11T20:13:33Z",
"operation": "TRANSFER",
"userId": "c266945c-f440-4537-85cf-a16b6e33b0cc",
"abiParameters": null,
"createDate": "2023-10-11T20:13:33Z",
"updateDate": "2023-10-11T20:13:45Z"
}
]
}
}
WHAT'S NEXT Congratulations! You’ve received your first transaction to your user-controlled wallet. To learn how wallet recovery works, go to