Wallet Payout

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

Overview

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

Pick the funding stablecoin with the asset field:

NetworkSupported assetsDefault
polygonUSDT, BRLAUSDT
baseUSDC, BRLAUSDC

BRLA is a BRL-pegged stablecoin, so it converts 1:1 to the PIX amount (no FX). USDT/USDC are converted at the current BRL rate.

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. 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.

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": "recipient@example.com",
    "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: 'recipient@example.com',
    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": "recipient@example.com",
        "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": "recipient@example.com",
    "pixKeyType": "EMAIL",
    "walletPin": "1234",
    "protectedSymmetricKey": "AoofiKHyVRLvdrknnXzo..."
  }'

Parameters

FieldTypeRequiredDescription
valueintegerYesAmount in BRL cents. Min 10 (R$ 0.10), max 100000 (R$ 1000).
networkstringYespolygon or base.
assetstringNoFunding stablecoin. polygon: USDT (default) or BRLA. base: USDC (default) or BRLA.
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 PIN typed by the end user.
protectedSymmetricKeystringYesThe value returned by POST /api/wallet/keys.
subAccountIdstringNoPay out from this subaccount's wallet (scoped to your API key). walletPin/protectedSymmetricKey must be the subaccount's. Defaults to the main account.

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 asset debited from the user. For BRLA it equals value + fee in BRL (1:1); for USDT/USDC it is the BRL total converted at the current rate.

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

Fees

fee = R$ 1.50 fixed + 2.5% of value

For value = 5000 (R$ 50.00), fee = R$ 2.75.

Errors

403 — feature flag disabled
{ "success": false, "error": "WALLET_PAYOUT_API feature flag is not enabled for this user" }
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" }
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": "recipient@example.com",
    "qrCode": null,
    "provider": "WOOVI",
    "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": "WOOVI",
    "correlationID": "c8d3...",
    "failureReason": null,
    "createdAt": "2026-04-28T23:32:10.000Z",
    "updatedAt": "2026-04-28T23:33:42.000Z"
  }
}
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 the chosen asset (USDT, USDC, or BRLA) to cover value + fee. If insufficient, the call returns 400 immediately — no funds are moved.

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