Wallet Transfer

Move USDT/USDC/BRLA/BRS between Hodle wallets or to any external address on Polygon, Base or Solana. Gas is sponsored.

Select a wallet

Pass walletId (the data.id returned by /api/wallet/create) to spend from that wallet, together with its protectedSymmetricKey and the platform walletPin. Also pass fromSubAccountId for a customer wallet. Unknown, removed, foreign or incorrectly scoped wallets fail without falling back to the default. Omit walletId to retain default selection. In sandbox, explicit selection spends from the selected PIN-protected Safe on Base Sepolia; the PIX leg remains simulated. See Wallet Keys.

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, BNB or SOL.

Pick the network and the token with the network and asset fields:

NetworkSupported assetsDefault asset
polygonUSDT, USDC, BRLAUSDT
baseUSDC, BRLA, BRS—
solanaUSDT, USDC, BRS—
bnbUSDT (BEP20)—

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.

BNB Chain is enabled per account. network: "bnb" (BNB Chain, chainId 56) moves USDT BEP20, which has 18 decimals on this network (not 6). It must be enabled on the account (ask support); without it the endpoint responds 403 with code: "BNB_FEATURE_DISABLED", and while the account's approval is pending or it is suspended, 403 with code: "BNB_PLANE_NOT_ACTIVE". The minimum is 1 USDT (400, code: "BNB_AMOUNT_BELOW_MINIMUM"). It is not available in sandbox: a sandbox key receives 400 with code: "BNB_SANDBOX_UNSUPPORTED". Any other network value is rejected with 400, never routed to another chain.

A BNB Chain transfer can answer 202. When the on-chain confirmation is not seen in time, the response is 202 with data.status: "PENDING", data.txHash: null and a data.userOpHash. The transfer was submitted and settles asynchronously: do not send it again. Send a reference so a retry with the same value returns 409 with the existing transfer instead of moving funds twice.

BRS is enabled per account. BRS transfers require BRS and API payouts enabled on the account — ask support. Without it the endpoint responds 403. BRS exists on solana and base; on base it is production-only (a sandbox key receives 403 with code: "BRS_BASE_PRODUCTION_ONLY") — see BRS.

The source is your main account by default. Pass fromSubAccountId to move funds out of a subaccount instead. For the recipient, pass either toSubAccountId (a subaccount under your platform) or recipientAddress for 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 walletPin and its protectedSymmetricKey; 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

FieldTypeRequiredDescription
fromSubAccountIdstringNoSource subaccount to debit. Defaults to your main account when omitted.
toSubAccountIdstringYes (or recipientAddress)Recipient subaccount under your platform.
networkstringNopolygon (default), base, solana or bnb.
assetstringNoToken to move. Defaults to USDT. See the table above for the pairs supported per network.
recipientAddressstringYes (or toSubAccountId)Destination address — EVM (0x...) on polygon/base/bnb, base58 on solana. Use this for external transfers.
amountstringYesAmount of asset to send, as a decimal string. Max 10000.
walletIdstringNoSource wallet ID returned as data.id by /api/wallet/create. Must match the owner, subaccount, network and supplied protected key. Omit for default selection.
walletPinstringYesThe 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.
protectedSymmetricKeystringYesThe source wallet's protectedSymmetricKey blob. Combined with walletPin to recover the signing key.
referencestringNoFree-form note stored on the transaction record for your reconciliation.

Provide exactly one of toSubAccountId or recipientAddress.

Response

200 OK
{
  "success": true,
  "data": {
    "txHash": "0xeafe9c4985963a7a7d6e49f763cca5c6006693031402d46c0da2fced4519fe03",
    "recipientAddress": "0x9b8c4d5e6f7081234567890abcdef0123456789a",
    "amount": "12.50"
  }
}
FieldTypeDescription
data.txHashstringHash of the on-chain transfer. Look it up on the explorer.
data.recipientAddressstringThe address that received the funds.
data.amountstringAmount transferred (echoes the request).

Errors

400 — both or neither recipient given
{ "success": false, "error": "Provide toSubAccountId or recipientAddress" }
400 — recipient could not be resolved
{ "success": false, "error": "Recipient address could not be resolved" }
400 — insufficient balance
{ "success": false, "error": "ERC20: transfer amount exceeds balance" }
400 — wrong walletPin / protectedSymmetricKey
{ "success": false, "error": "Invalid PIN" }
400 — asset not supported on the network
{ "success": false, "error": "BRLA is not supported on solana" }
403 — asset not enabled on the account
{ "success": false, "error": "BRS is not enabled for this account. Please send a message to support." }
401 — missing or invalid API key
{ "success": false, "error": "Unauthorized" }

When to use this vs. wallet/payout

  • /api/wallet/transfer moves stablecoin on-chain. The recipient ends up holding the token you sent.
  • /api/wallet/payout moves stablecoin out of the wallet and settles the receiver in BRL via PIX. Use it when the recipient is a Brazilian PIX key.