Wallet Payout

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

Overview

/api/wallet/payout triggers a PIX payout funded by the user's USDT (Polygon), USDC (Base), or USDT-TRC20 (Tron) balance. You make one POST and one GET — Hodle does the rest.

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()

Parameters

FieldTypeRequiredDescription
valueintegerYesAmount in BRL cents. Min 10 (R$ 0.10), max 100000 (R$ 1000).
networkstringYespolygon (USDT-ERC20), base (USDC), or tron (USDT-TRC20).
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.

Response

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

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" }]
}
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 USDT/USDC 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.