Wallet Transfer
Move USDT/USDC/BRLA/BRS between Hodle wallets or to any external address on Polygon, Base or Solana. Gas is sponsored.
POST /api/wallet/transfer
Transfers stablecoin from the API key user's wallet to either another linked user or an external address. Gas is paid by Hodle — the user does not need MATIC, ETH or SOL.
Pick the network and the token with the network and asset fields:
| Network | Supported assets | Default asset |
|---|---|---|
polygon | USDT, USDC, BRLA | USDT |
base | USDC, BRLA | — |
solana | USDT, USDC, BRS | — |
network defaults to polygon and asset defaults to USDT, so omitting both keeps the original USDT-on-Polygon behaviour.
On solana the transfer is an SPL transferChecked signed with the user's Solana key; Hodle's master wallet pays the network fee and, on a first transfer to a fresh recipient, the rent of the recipient's associated token account.
BRS is gated by the Nora provider flag.
BRStransfers require theNORA_RAILper-user feature flag in addition toWALLET_PAYOUT_API. Without it the endpoint responds403.BRSexists only onsolana— see BRS.
The source is your main account by default. Pass
fromSubAccountIdto move funds out of a subaccount instead. For the recipient, pass eithertoSubAccountId(a subaccount under your platform) orrecipientAddressfor an external transfer. Both subaccounts must belong to your platform, so subaccount A → subaccount B transfers stay internal.
Self-custodial signing. These wallets are non-custodial — Hodle stores only ciphertext and can never sign on its own. Every transfer must carry the source wallet's
walletPinand itsprotectedSymmetricKey; the server uses them to unlock the mnemonic transiently in memory to sign, then discards them. You (the platform) are the custodian of your subaccounts' PINs and must store and pass them per request.
Request
curl --request POST \
--url https://api.hodle.com.br/api/wallet/transfer \
--header "Authorization: Bearer $API_KEY" \
--header "Content-Type: application/json" \
--data '{
"toSubAccountId": "5b9f1a83b6b7c2b001f3c9e21",
"amount": "5.00",
"walletPin": "424242",
"protectedSymmetricKey": "AoofiKHyVRLvdrknnXzoIh1Gd1YTwLaOBn4ibm103a4d...",
"reference": "internal-payroll-#1029"
}'const res = await fetch('https://api.hodle.com.br/api/wallet/transfer', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.HODLE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipientAddress: '0x9b8c4d5e6f7081234567890abcdef0123456789a',
amount: '12.50',
walletPin: '424242',
protectedSymmetricKey: 'AoofiKHyVRLvdrknnXzoIh1Gd1YTwLaOBn4ibm103a4d...',
}),
})
const data = await res.json()import os, requests
res = requests.post(
"https://api.hodle.com.br/api/wallet/transfer",
headers={
"Authorization": f"Bearer {os.environ['HODLE_API_KEY']}",
"Content-Type": "application/json",
},
json={
"recipientAddress": "0x9b8c4d5e6f7081234567890abcdef0123456789a",
"amount": "12.50",
"walletPin": "424242",
"protectedSymmetricKey": "AoofiKHyVRLvdrknnXzoIh1Gd1YTwLaOBn4ibm103a4d...",
},
)
data = res.json()Parameters
| Field | Type | Required | Description |
|---|---|---|---|
fromSubAccountId | string | No | Source subaccount to debit. Defaults to your main account when omitted. |
toSubAccountId | string | Yes (or recipientAddress) | Recipient subaccount under your platform. |
network | string | No | polygon (default), base or solana. |
asset | string | No | Token to move. Defaults to USDT. See the table above for the pairs supported per network. |
recipientAddress | string | Yes (or toSubAccountId) | Destination address — EVM (0x...) on polygon/base, base58 on solana. Use this for external transfers. |
amount | string | Yes | Amount of asset to send, as a decimal string. Max 10000. |
walletPin | string | Yes | The wallet PIN the user established on the Hodle platform (see how the PIN is established). Used to unlock the wallet transiently for signing; never stored. |
protectedSymmetricKey | string | Yes | The source wallet's protectedSymmetricKey blob. Combined with walletPin to recover the signing key. |
reference | string | No | Free-form note stored on the transaction record for your reconciliation. |
Provide exactly one of toSubAccountId or recipientAddress.
Response
{
"success": true,
"data": {
"txHash": "0xeafe9c4985963a7a7d6e49f763cca5c6006693031402d46c0da2fced4519fe03",
"recipientAddress": "0x9b8c4d5e6f7081234567890abcdef0123456789a",
"amount": "12.50"
}
}| Field | Type | Description |
|---|---|---|
data.txHash | string | Hash of the on-chain transfer. Look it up on the explorer. |
data.recipientAddress | string | The address that received the funds. |
data.amount | string | Amount transferred (echoes the request). |
Errors
{ "success": false, "error": "Provide toSubAccountId or recipientAddress" }{ "success": false, "error": "Recipient address could not be resolved" }{ "success": false, "error": "ERC20: transfer amount exceeds balance" }{ "success": false, "error": "Invalid PIN" }{ "success": false, "error": "BRLA is not supported on solana" }{ "success": false, "error": "BRS is not enabled for this account. Please send a message to support." }{ "success": false, "error": "Unauthorized" }When to use this vs. wallet/payout
/api/wallet/transfermoves stablecoin on-chain. The recipient ends up holding the token you sent./api/wallet/payoutmoves stablecoin out of the wallet and settles the receiver in BRL via PIX. Use it when the recipient is a Brazilian PIX key.