Wallet Transfer

Move USDT/USDC between Hodle wallets or to any external address. Gas is sponsored.

POST /api/wallet/transfer

Transfers stablecoin from the API key user's wallet to either another linked user or an external EVM address. Gas is paid by Hodle — the user does not need MATIC or ETH.

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.
recipientAddressstringYes (or toSubAccountId)EVM address (0x...). Use this for external transfers.
amountstringYesAmount in the wallet's stablecoin (USDT/USDC), as a decimal string.
walletPinstringYesThe source wallet's PIN (or password, before a PIN is set). 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" }
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 USDT/USDC.
  • /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.