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:
| Network | Supported assets | Automatic order |
|---|---|---|
polygon | USDT, USDC, BRLA | BRLA → USDT → USDC |
base | USDC, BRLA | BRLA → USDC |
tron | USDT | USDT |
solana | USDT, USDC, BRS | USDT → USDC |
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 returnserrorCode: 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 theTRON_PAYOUTper-user feature flag in addition toWALLET_PAYOUT_API. Without it the endpoint responds403fornetwork: "tron".
BRS is gated by the Nora provider flag.
BRSpayouts require theNORA_RAILper-user feature flag in addition toWALLET_PAYOUT_API. Without it the endpoint responds403for"asset": "BRS".BRSexists only onsolana— see BRS.
You must call
POST /api/wallet/keysfirst to get the user'sprotectedSymmetricKey. Cache it on your side — fetch only once per user.Gated by feature flag. All three endpoints require the
WALLET_PAYOUT_APIper-user feature flag. Without it the endpoints respond403. Contact Hodle to enable it.
Flow
POST /api/wallet/keys— once per user. Cache the response.POST /api/wallet/payout— every payout. Resolves the asset and fee before signing, then returns202immediately with atransactionId.GET /api/wallet/payout/{transactionId}— poll every 5s untilCOMPLETEDorFAILED.
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
| Field | Type | Required | Description |
|---|---|---|---|
value | integer | Yes | Amount 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. |
network | string | Yes | polygon, base, tron, or solana. |
asset | string | No | Funding preference. polygon: BRLA → USDT/USDC. base: BRLA → USDC. tron: USDT only. solana: omitted uses USDT → USDC; explicit USDT or USDC is used directly. BRS is explicit-only and feature-gated. |
pixKey | string | Yes (or qrCode) | PIX key of the recipient. Mutually exclusive with qrCode. |
pixKeyType | string | Yes (when using pixKey) | One of PHONE, CPF, EMAIL, RANDOM, CNPJ. |
qrCode | string | Yes (or pixKey) | PIX BR Code copy-paste. Mutually exclusive with pixKey. |
walletPin | string | Yes | The 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. |
protectedSymmetricKey | string | Yes | The value returned by POST /api/wallet/keys. |
externalId | string | No | Your 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). |
subAccountId | string | No | Pay out from this subaccount's wallet (scoped to your API key). walletPin/protectedSymmetricKey must be the subaccount's. Defaults to the main account. |
taxId | string | No | CPF 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
{
"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
{ "success": false, "error": "WALLET_PAYOUT_API feature flag is not enabled for this user" }{ "success": false, "error": "Third party operations does not enabled to your company, call with support" }{ "success": false, "error": "Valor excede o limite por transação de ... R$ 3000,00", "errorCode": "PER_TX_LIMIT_EXCEEDED" }{ "success": false, "error": "Invalid PIN" }{ "success": false, "error": "Too many invalid PIN attempts. Locked for 24h." }{ "success": false, "error": "Failed to prepare full UserOp: ... ERC20: transfer amount exceeds balance ..." }{
"success": false,
"error": "Validation failed",
"details": [{ "field": "pixKeyType", "message": "pixKeyType is required when pixKey is provided" }]
}{ "success": false, "error": "USDT is not supported on base" }{
"success": false,
"error": "Insufficient balance for the payout",
"errorCode": "INSUFFICIENT_BALANCE"
}{ "success": false, "error": "Wait 28 seconds before retrying" }{ "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
{
"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"
}
}{
"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.
{
"success": true,
"data": {
"transactionId": "65f1a8...",
"status": "FAILED",
"txHash": "0xeafe9c...",
"failureReason": "...",
"...": "..."
}
}Status values
| Status | Meaning |
|---|---|
PENDING | Payout in progress. Keep polling. |
COMPLETED | On-chain transfer mined and PIX confirmed. |
FAILED | Something went wrong. failureReason explains what. |
REFUNDED | Reserved (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.