Wallet Payout

Pay PIX from a user's USDT, USDC, BRLA or BRS balance on Polygon, Base, Tron or Solana in a single API call.

Overview

/api/wallet/payout triggers a PIX payout funded by the user's stablecoin balance on Polygon, Base, Tron or Solana. You make one POST and one GET — Hodle does the rest.

This is also the endpoint for paying a PIX BR Code (QR) from a balance the user already holds — pass the scanned code as qrCode. It is the only PIX off-ramp the platform offers for stablecoin balances.

asset is an optional funding preference. On Polygon and Base, Hodle prefers BRLA because it avoids the more expensive USD-stablecoin conversion route, then falls back to the requested/provider asset only when BRLA cannot cover the complete debit plus fee:

NetworkSupported assetsAutomatic order
polygonUSDT, USDC, BRLABRLAUSDTUSDC
baseUSDC, BRLABRLAUSDC
tronUSDTUSDT
solanaUSDT, USDC, BRSUSDTUSDC

Fallback is balance-aware and happens before signing. When a candidate cannot cover the full payout debit plus its fee with INSUFFICIENT_BALANCE, Hodle tries the next candidate. No intermediate USDC/USDT → BRLA swap is performed, and no partial debit is created. If every candidate is insufficient, the request returns errorCode: INSUFFICIENT_BALANCE.

asset can be used to choose the direct provider fallback. For example, a Polygon request with asset: "USDC" tries BRLA first and uses USDC directly if BRLA cannot cover the payout. An explicit Solana USDC request uses USDC directly. Tron always debits USDT for compatibility, regardless of asset.

BRLA and BRS are BRL-pegged stablecoins, so they convert 1:1 to the PIX amount (no FX). USDT/USDC are converted at the current BRL rate.

On solana the debit is an SPL transferChecked signed with the user's Solana key; Hodle's master wallet pays the network fee, so the user never needs SOL.

Tron is gated by its own flag. Tron payouts (USDT-TRC20 only) require the TRON_PAYOUT per-user feature flag in addition to WALLET_PAYOUT_API. Without it the endpoint responds 403 for network: "tron".

BRS is gated by the Nora provider flag. BRS payouts require the NORA_RAIL per-user feature flag in addition to WALLET_PAYOUT_API. Without it the endpoint responds 403 for "asset": "BRS". BRS exists only on solana — see BRS.

You must call POST /api/wallet/keys first to get the user's protectedSymmetricKey. Cache it on your side — fetch only once per user.

Gated by feature flag. All three endpoints require the WALLET_PAYOUT_API per-user feature flag. Without it the endpoints respond 403. Contact Hodle to enable it.

Flow

  1. POST /api/wallet/keys — once per user. Cache the response.
  2. POST /api/wallet/payout — every payout. Resolves the asset and fee before signing, then returns 202 immediately with a transactionId.
  3. GET /api/wallet/payout/{transactionId} — poll every 5s until COMPLETED or FAILED.

A typical run reaches COMPLETED in 30–90 seconds. A BRS payout can remain PENDING longer when Hodle's operating balance is below the requested amount: Hodle burns the BRS with Nora, waits for the corresponding BRL credit, and only then releases the customer's PIX.

POST /api/wallet/payout

Request

curl --request POST \
  --url https://api.hodle.com.br/api/wallet/payout \
  --header "Authorization: Bearer $API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "value": 5000,
    "network": "polygon",
    "pixKey": "[email protected]",
    "pixKeyType": "EMAIL",
    "walletPin": "1234",
    "protectedSymmetricKey": "AoofiKHyVRLvdrknnXzo..."
  }'
const res = await fetch('https://api.hodle.com.br/api/wallet/payout', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.HODLE_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    value: 5000,
    network: 'polygon',
    pixKey: '[email protected]',
    pixKeyType: 'EMAIL',
    walletPin: '1234',
    protectedSymmetricKey: 'AoofiKHyVRLvdrknnXzo...',
  }),
})
const data = await res.json()
import os, requests

res = requests.post(
    "https://api.hodle.com.br/api/wallet/payout",
    headers={
        "Authorization": f"Bearer {os.environ['HODLE_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "value": 5000,
        "network": "polygon",
        "pixKey": "[email protected]",
        "pixKeyType": "EMAIL",
        "walletPin": "1234",
        "protectedSymmetricKey": "AoofiKHyVRLvdrknnXzo...",
    },
)
data = res.json()

To fund the payout with BRLA instead, add "asset": "BRLA" (works on both polygon and base):

curl --request POST \
  --url https://api.hodle.com.br/api/wallet/payout \
  --header "Authorization: Bearer $API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "value": 5000,
    "network": "base",
    "asset": "BRLA",
    "pixKey": "[email protected]",
    "pixKeyType": "EMAIL",
    "walletPin": "1234",
    "protectedSymmetricKey": "AoofiKHyVRLvdrknnXzo..."
  }'

Parameters

FieldTypeRequiredDescription
valueintegerYesAmount in BRL cents. Min 10 (R$ 0.10). No fixed maximum — the ceiling is your per-account per-transaction limit (perTxLimitBrl, default R$ 3000). Ask Hodle to raise it.
networkstringYespolygon, base, tron, or solana.
assetstringNoFunding preference. polygon: BRLAUSDT/USDC. base: BRLAUSDC. tron: USDT only. solana: omitted uses USDTUSDC; explicit USDT or USDC is used directly. BRS is explicit-only and feature-gated.
pixKeystringYes (or qrCode)PIX key of the recipient. Mutually exclusive with qrCode.
pixKeyTypestringYes (when using pixKey)One of PHONE, CPF, EMAIL, RANDOM, CNPJ.
qrCodestringYes (or pixKey)PIX BR Code copy-paste. Mutually exclusive with pixKey.
walletPinstringYesThe wallet PIN the user established on the Hodle platform (see Wallet PIN model). Combined server-side with email + protectedSymmetricKey to unlock the wallet transiently for signing; never stored. Required for polygon, base, tron, and solana.
protectedSymmetricKeystringYesThe value returned by POST /api/wallet/keys.
externalIdstringNoYour own idempotency key. A retry with the same value returns the original payout (alreadyProcessed: true) instead of paying twice, however far apart the attempts are. Without it duplicates are only caught heuristically (same user, value and key inside a short window).
subAccountIdstringNoPay out from this subaccount's wallet (scoped to your API key). walletPin/protectedSymmetricKey must be the subaccount's. Defaults to the main account.
taxIdstringNoCPF of the beneficiary receiving the PIX, digits only. Omit when paying the account holder. A value that differs from the subaccount's KYC taxId is a third-party operation and needs that enabled — see Third-party operations.

Response

202 Accepted
{
  "success": true,
  "transactionId": "65f1a8...",
  "status": "PROCESSING",
  "stableAmount": "9.31",
  "valueInBrl": "50.00",
  "fee": "1.55",
  "network": "polygon",
  "asset": "USDT",
  "jobId": "1"
}

stableAmount is the amount of the selected asset debited from the user. For BRLA and BRS it equals value + fee in BRL (1:1); for USDT/USDC it is the BRL total converted at the current rate. The response's asset is the asset that actually passed the balance check and was signed.

The on-chain transfer is not yet final at this point. Use the GET endpoint to wait for confirmation.

Fees

The final fee is calculated again for each candidate before its balance check. Unless the account has custom pricing, the default base fee on stablecoin networks is R$ 1.50 fixed + 2.5% of value; affiliate discounts are applied afterward. Accounts with the PAYOUT_ASSET_FEE flag can have a fixed or percentage fee per asset, with its configured minimum floor. The selected transaction stores the fee for the asset that was actually debited.

With default pricing and no affiliate discount, value = 5000 (R$ 50.00) produces a fee of R$ 2.75.

Errors

403 — feature flag disabled
{ "success": false, "error": "WALLET_PAYOUT_API feature flag is not enabled for this user" }
403 — third-party payout not enabled
{ "success": false, "error": "Third party operations does not enabled to your company, call with support" }
403 — over the per-transaction limit
{ "success": false, "error": "Valor excede o limite por transação de ... R$ 3000,00", "errorCode": "PER_TX_LIMIT_EXCEEDED" }
400 — invalid PIN
{ "success": false, "error": "Invalid PIN" }
400 — locked after 3 wrong PINs
{ "success": false, "error": "Too many invalid PIN attempts. Locked for 24h." }
400 — insufficient stable balance
{ "success": false, "error": "Failed to prepare full UserOp: ... ERC20: transfer amount exceeds balance ..." }
400 — pixKeyType missing
{
  "success": false,
  "error": "Validation failed",
  "details": [{ "field": "pixKeyType", "message": "pixKeyType is required when pixKey is provided" }]
}
400 — asset not supported on network
{ "success": false, "error": "USDT is not supported on base" }
400 — no candidate asset can cover the payout
{
  "success": false,
  "error": "Insufficient balance for the payout",
  "errorCode": "INSUFFICIENT_BALANCE"
}
429 — rate limit
{ "success": false, "error": "Wait 28 seconds before retrying" }
503 — globally disabled
{ "success": false, "error": "PIX payments are temporarily disabled" }

GET /api/wallet/payout/{transactionId}

Poll for the final state.

Request

curl --request GET \
  --url "https://api.hodle.com.br/api/wallet/payout/65f1a8..." \
  --header "Authorization: Bearer $API_KEY"
const res = await fetch(
  `https://api.hodle.com.br/api/wallet/payout/${transactionId}`,
  { headers: { Authorization: `Bearer ${process.env.HODLE_API_KEY}` } },
)
const data = await res.json()
import os, requests

res = requests.get(
    f"https://api.hodle.com.br/api/wallet/payout/{transaction_id}",
    headers={"Authorization": f"Bearer {os.environ['HODLE_API_KEY']}"},
)
data = res.json()

Response

status: PENDING
{
  "success": true,
  "data": {
    "transactionId": "65f1a8...",
    "status": "PENDING",
    "network": "polygon",
    "txHash": null,
    "endToEndId": null,
    "valueInBrl": "50.00",
    "fee": "1.55",
    "pixKey": "[email protected]",
    "qrCode": null,
    "provider": "configured-provider",
    "correlationID": null,
    "failureReason": null,
    "createdAt": "2026-04-28T23:32:10.000Z",
    "updatedAt": "2026-04-28T23:32:10.000Z"
  }
}
status: COMPLETED
{
  "success": true,
  "data": {
    "transactionId": "65f1a8...",
    "status": "COMPLETED",
    "network": "polygon",
    "txHash": "0xeafe9c4985963a7a7d6e49f763cca5c6006693031402d46c0da2fced4519fe03",
    "endToEndId": "E12345678202604281432abcdef123456",
    "valueInBrl": "50.00",
    "fee": "1.55",
    "provider": "configured-provider",
    "correlationID": "c8d3...",
    "failureReason": null,
    "receipt": {
      "endToEndId": "E12345678202604281432abcdef123456",
      "paidAt": "2026-04-28T23:33:41.708Z",
      "rail": "configured-rail",
      "amountInBrl": "50.00",
      "payerIspb": "12345678",
      "receiver": {
        "name": "MARIA SOUZA",
        "taxId": "***.241.413-**",
        "pixKey": "[email protected]",
        "bankName": "Example Bank",
        "ispb": "54811417",
        "branch": "0001",
        "account": "****5716",
        "accountType": "TRAN"
      }
    },
    "createdAt": "2026-04-28T23:32:10.000Z",
    "updatedAt": "2026-04-28T23:33:42.000Z"
  }
}

receipt is the same object delivered on the PAYOUT_SUCCESSFUL webhook — the payee, the institution, the settlement time and the end-to-end id, ready to render your own receipt. It is null until the PIX settles, and stays null for a payout that settled before this field existed.

status: FAILED
{
  "success": true,
  "data": {
    "transactionId": "65f1a8...",
    "status": "FAILED",
    "txHash": "0xeafe9c...",
    "failureReason": "...",
    "...": "..."
  }
}

Status values

StatusMeaning
PENDINGPayout in progress. Keep polling.
COMPLETEDOn-chain transfer mined and PIX confirmed.
FAILEDSomething went wrong. failureReason explains what.
REFUNDEDReserved (not produced by this endpoint today).

Funding requirements

The user's wallet must have enough of a candidate asset (USDT, USDC, BRLA, or BRS) to cover value + fee. Hodle checks each candidate against the complete debit before signing. If a candidate is insufficient, the next eligible asset is tried; if every candidate is insufficient, the call returns 400 and no funds are moved.

The balance is checked against each asset separately, not the wallet as a whole. There is no automatic swap between stablecoins. The eventual transaction records the asset, fee and FX rate selected by the successful candidate. If the on-chain debit is confirmed but the PIX leg later fails, the refund uses that same asset and network.

For BRS, insufficient BRL in Hodle's operating account is not a terminal PIX failure. The BRS already received by Hodle is burned through Nora, the transaction stays PENDING, and the payout resumes after Nora's BRL reaches Hodle. This may take roughly 18 minutes.

No candidate asset can cover the payout: required value plus fee exceeds the
available balance for every eligible asset.

Any supported asset sent directly to the wallet's network address funds a payout — there is no separate deposit step to register or settle it, for BRLA or for anything else. Balances are read from chain, so a confirmed transfer is spendable as soon as wallet/get reflects it (pass sync: true to force a fresh read).

Gas is sponsored by Hodle. The user does not need MATIC or ETH.