Skip to content
LIQFYdocs
PTENENEN
Go to dashboard

Payouts (Withdrawals / Saque)

O termo de produto é Saque (payout); o endpoint atual /v1/withdrawals é o nome de fio (wire format) em uso hoje e ainda não foi renomeado para /v1/payouts.

Move balance out of your Liqfy wallet to a real-world destination — a PIX key (Brazil) or a bank account (TED). This is the inverse of PIX payments: payments credit your wallet, payouts debit it.

Look up by order number: send that number as Idempotency-Key when creating the payout. Later call GET /v1/withdrawals/by-idempotency-key/{order-number} with an API key for the same account. A payout created without this key cannot be found by order number. See lookup examples.

Receipt: GET /v1/withdrawals/{id}/receipt?format=json returns structured data; omitting format still returns the PDF envelope {contentType, base64}. See Receipt and JSON receipt contract.

The payout.paid callback can include the Pix E2E in data.object.settlement.end_to_end_id once confirmed. For split payouts, read each E2E in settlement.legs[]; a missing E2E does not mean the payout failed. See Webhooks.

Tracks are per account. TED depends on the corresponding module being enabled for you. Confirm with your Liqfy contact before building on top of one of them.

#Flow at a glance

text
1. POST /v1/withdrawals/validate-pix-key   (optional — pre-flight)
2. POST /v1/withdrawals                    (creates the request and reserves the funds right away)
3. Liqfy dispatches — automatically or after approval
4. Webhook payout.paid (or payout.failed) hits your endpoint

Payouts use the same API key as charges — Authorization: Bearer <key>, called from your server, never from the end customer's browser or app. (The apikey header is still accepted.) API-key requests are also subject to your account's IP allowlist. The wallet is debited at create time; whether the payout also needs approval depends on your account settings.

Always send the Idempotency-Key header on create — it is the primary idempotency channel (see Create the payout).

Before creating the payout, check the destination key is well-formed.

POST /v1/withdrawals/validate-pix-key

json
{
  "key": "12345678909",
  "type": "CPF"
}

Response 201 Created

json
{
  "isValid": true,
  "type": "CPF",
  "normalized": "12345678909"
}

Supported type values

TypeFormat
CPF11 digits (dots and dashes are stripped), mod-11 checksum. Numbers with all digits equal are rejected.
CNPJ14 digits (dots, dashes and slashes are stripped), mod-11 checksum. Numbers with all digits equal are rejected.
EMAILlocal@domain.tld shape, lowercased, max 77 chars.
PHONE+55 followed by 10 or 11 digits (+5511999999999). Spaces, dashes and parentheses are stripped.
RANDOMRandom key — UUID v4, lowercased.
(omitted)Auto-detected from the key value.

The type is case-insensitive. A declared known type is validated strictly — declaring CPF for an e-mail fails. Declaring any other type (for example EVP) is ignored: the key is auto-detected and the response reports the detected type.

Auto-detection order: UUID v4 → RANDOM; starts with +55 → PHONE; e-mail shape → EMAIL; 11 digits → CPF; 14 digits → CNPJ. A phone number sent without +55 (11 digits) is therefore detected as CPF — send phones in E.164 or declare PHONE. When nothing matches, the answer is isValid: false with type: "CPF" as a placeholder and error: "Unable to detect PIX key type".

The endpoint answers 201 even when the key is invalid — a failure lives in the body, never in the HTTP status. Only a missing or empty key is a 400.

Failure 201 Created

json
{
  "isValid": false,
  "type": "CPF",
  "normalized": "12345678900",
  "error": "CPF checksum invalid"
}

Possible error values

errorType
CPF must be 11 digits / CPF checksum invalidCPF
CNPJ must be 14 digits / CNPJ checksum invalidCNPJ
Invalid email format / Email exceeds 77 charactersEMAIL
Phone must start with +55 / Phone must have 10 or 11 digits after +55PHONE
Random key must be a valid UUID v4RANDOM
Unable to detect PIX key typeauto-detection failed
PIX key must not be emptywhitespace-only key

#2. Quote, fee and withdrawable balance

Three read-only helpers, same API key as the create endpoint. currency is optional in all of them (default BRL).

#Quote — what to send so the recipient gets an exact amount

POST /v1/withdrawals/quote — body { value, currency? }, value an integer greater than zero. Priced as PIX. Invalid body → 400.

Response 201 Created

json
{
  "value": 50000,
  "fee": 100,
  "total": 50100
}

total is the gross you pay so that the recipient receives exactly value — the fee is grossed up so the destination never absorbs it.

bash
curl -X POST https://api.liqfy.com.br/v1/withdrawals/quote \
  -H "Authorization: Bearer $LIQFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "value": 50000 }'
ts
import { LiqfyClient } from '@liqfy/node';

const client = new LiqfyClient({ apiKey: process.env.LIQFY_API_KEY! });

const quote = await client.payouts.quote({ value: 50000 });

Python client.payouts.quote(value=50000), PHP $client->payouts->quote(50000), .NET client.Payouts.QuoteAsync(50000).

#Fee — what a create would charge

GET /v1/withdrawals/payout-fee

QueryRequiredDescription
amountyesInteger, 1 to 10.000.000.000 (smallest unit).
currencynoDefault BRL. 3 to 8 characters.
typenoPIX (default) or TED.

Any value outside these bounds → 400.

Response 200 OK

json
{
  "amount": 50000,
  "currency": "BRL",
  "fee": 100,
  "total": 50100
}

fee is what POST /v1/withdrawals would charge for that amount (already including the split-leg complement above the PIX cap); total = amount + fee.

bash
curl "https://api.liqfy.com.br/v1/withdrawals/payout-fee?amount=50000&currency=BRL&type=PIX" \
  -H "Authorization: Bearer $LIQFY_API_KEY"
ts
const fee = await client.payouts.payoutFee({ amount: 50000 });

Python client.payouts.payout_fee(amount=50000), PHP $client->payouts->payoutFee(50000), .NET client.Payouts.PayoutFeeAsync(50000).

#Withdrawable balance and fee parameters

GET /v1/withdrawals/payout-info — no side effects, built for the "withdraw everything" button.

Response 200 OK

json
{
  "currency": "BRL",
  "available": 1000000,
  "pendingAdjustment": 0,
  "fee": { "fixed": 100, "variablePct": 0, "minimum": 100 },
  "maxWithdrawable": 999900
}

available is the spendable balance. maxWithdrawable is the largest gross amount requestable with the fee already accounted. fee is the parameter set applied today (fixed, variablePct, minimum).

bash
curl "https://api.liqfy.com.br/v1/withdrawals/payout-info?currency=BRL" \
  -H "Authorization: Bearer $LIQFY_API_KEY"
ts
const info = await client.payouts.payoutInfo();

Python client.payouts.payout_info(), PHP $client->payouts->payoutInfo(), .NET client.Payouts.PayoutInfoAsync().

#3. Create the payout

POST /v1/withdrawals

Idempotency. Send the Idempotency-Key header — the primary channel, trimmed, at most 255 characters (a longer header → 400 "Idempotency-Key must be at most 255 characters"). The body field idempotencyKey (1 to 255 characters) is a fallback for non-SDK callers; when both are present the header wins. The same key returns the same payout: 201 again, no second debit. If the reserve fails, the row is marked REJECTED and the key is released — after topping up you can retry with the same key.

Without a key, the only protection is a near-duplicate guard: the same type, amount and pixKey within 90 seconds answers 409 with no stable error.code. Send an idempotency key and you never depend on it.

FieldTypeRequiredDescription
typeenumyesPIX or TED. (CRYPTO exists but is dashboard-only — see below.)
amountintegeryesPositive integer, smallest currency unit. In the default gross mode it is what the recipient receives; the fee is debited on top.
currencystringnoDefault BRL.
pixKeystringon PIXDestination PIX key.
pixKeyTypeenumnoCPF, CNPJ, EMAIL, PHONE, RANDOM. Omitted or unknown (for example EVP): ignored, key auto-detected.
bankCodestringon TEDBank code (FEBRABAN).
bankAgencystringon TEDBranch (agência).
bankAccountstringon TEDAccount number.
walletIdstringnoExplicit spendable OPERATIONAL wallet in currency. Omitted → auto-select (the currency's default wallet if it has balance, else any spendable wallet with enough balance). Unknown → WALLET_NOT_FOUND; not spendable → WALLET_NOT_SPENDABLE; nothing covers the debit → INSUFFICIENT_BALANCE.
amountTypeenumnogross (default) or net. net is legacy and still accepted: the debit is amount and the recipient gets amount − fee (fee >= amount → 400).
idempotencyKeystringnoFallback — prefer the Idempotency-Key header.
totpCodestringnoDashboard only: the 2FA code when the account requires 2FA on withdrawals. Ignored on API-key requests.

Unknown keys (description, cryptoAddress, cryptoNetwork, cryptoCurrency, anything else) are silently stripped — the API never stores or returns them. cryptoAddress, cryptoNetwork and cryptoCurrency can still appear as read-only legacy response fields; they are not create inputs.

Crypto payouts are dashboard-only. type: "CRYPTO" (with cryptoWalletAddressId and cryptoEstimateId) is accepted only from a logged-in dashboard user; with an API key it answers 403 with the token CRYPTO_PANEL_ONLY. Same for POST /v1/withdrawals/crypto/estimate; GET /v1/withdrawals/crypto/availability answers { "enabled": false } to API keys. In the dashboard a crypto payout must be in BRL, from R$ 5.000,00 (500000), not net, with both ids (otherwise 400 INVALID_CRYPTO_WITHDRAWAL), and always asks for the 2FA code.

Example — PIX payout

bash
curl -X POST https://api.liqfy.com.br/v1/withdrawals \
  -H "Authorization: Bearer $LIQFY_API_KEY" \
  -H "Idempotency-Key: payout-2026-W17" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "PIX",
    "amount": 50000,
    "pixKey": "12345678909",
    "pixKeyType": "CPF"
  }'

Example — TED payout (the bank details are three flat strings)

bash
curl -X POST https://api.liqfy.com.br/v1/withdrawals \
  -H "Authorization: Bearer $LIQFY_API_KEY" \
  -H "Idempotency-Key: payout-2026-W17-ted" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "TED",
    "amount": 50000,
    "bankCode": "001",
    "bankAgency": "1234",
    "bankAccount": "56789012"
  }'
ts
const payout = await client.payouts.create(
  { type: 'PIX', amount: 50000, pixKey: '12345678909', pixKeyType: 'CPF' },
  { idempotencyKey: 'payout-2026-W17' },
);
// TED: { type: 'TED', amount: 50000, bankCode: '001', bankAgency: '1234', bankAccount: '56789012' }

Python client.payouts.create(input, idempotency_key=…), PHP $client->payouts->create($input, $idempotencyKey), .NET client.Payouts.CreateAsync(input, idempotencyKey).

Response 201 Created

json
{
  "id": "f6a7b8c9-d0e1-4234-9f01-234567890123",
  "type": "PIX",
  "status": "PENDING",
  "amount": 50000,
  "fee": 100,
  "netAmount": 50000,
  "currency": "BRL",
  "source": "API",
  "walletId": "2c9f8e7d-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
  "pixKey": "12345678909",
  "pixKeyType": "CPF",
  "idempotencyKey": "payout-2026-W17",
  "createdAt": "2026-04-25T16:10:00.000Z",
  "updatedAt": "2026-04-25T16:10:00.000Z"
}

The wallet is debited at create. In gross mode (the default) the debit is amount + fee (here 50.100) and the recipient receives exactly amount — that is why netAmount equals amount. The funds are reserved from this moment on; approval only decides when the payout is dispatched.

A 201 can already come back APPROVED or PROCESSING: auto-payout approves and dispatches immediately when your account policy allows it. "Always starts PENDING" is not guaranteed.

When a payout doesn't go through. The full refusal contract — the error.code for an immediate balance refusal, the failure codes (failure_code), and which of them refund automatically — is in Payout failures.

#Creating from the dashboard

  • Only the account owner or an admin can create a payout in the dashboard (a regular member gets 403, code = WITHDRAWAL_CANCEL_ROLE_REQUIRED).
  • When the account requires 2FA on withdrawals (always, for crypto), the request needs a fresh totpCode. A user without 2FA enabled gets 403 ("Esta conta exige 2FA para saques…"); a missing or invalid code gets 401 ("Código 2FA inválido ou ausente.").
  • Payouts created in the dashboard have source: "PANEL"; via API key, source: "API".

#Refused requests leave a trail

Every 4xx refusal of POST /v1/withdrawals (balance, KYC, validation, limits, rules…) is recorded in the account's audit log as WITHDRAWAL_REJECTED and shows up on the withdrawals screen, with amount, origin and reason — for example "Pedido de saque de R$ 500,00 recusado: saldo insuficiente (disponível R$ 120,00), via API". The same request repeated with the same idempotency key and the same refusal code is recorded once per hour. Refusals by the IP allowlist happen before this step and are not recorded here.

#4. Lifecycle

text
PENDING ─▶ APPROVED ─▶ PROCESSING ─▶ CONFIRMING ─▶ COMPLETED            ✓ funds delivered
                                                    └▶ PARTIALLY_COMPLETED   delivered in parts

Terminal outcomes also include FAILED, REJECTED, CANCELLED and REVERSED. Ten statuses in total; the six terminal ones (COMPLETED, PARTIALLY_COMPLETED, FAILED, REJECTED, CANCELLED, REVERSED) never change again.

StatusWallet effect
PENDINGDebited at create (amount + fee reserved)
APPROVEDReserved — approved, waiting dispatch
PROCESSINGReserved — sent for settlement
CONFIRMINGReserved — waiting the receiving institution's confirmation
COMPLETEDDebited (terminal) — delivered
PARTIALLY_COMPLETEDDebited (terminal) — some legs delivered, others failed (see legs); the failed legs are refunded
FAILEDAuto-refunded — dispatch failed (failure_code). The refund can be held until confirmed (see Payout failures)
REJECTEDRefunded — refused at review, or by the reserve (system:reserve-failed)
CANCELLEDRefunded (amount + fee) — cancelled by you before dispatch, or when an admin deactivates the account (reason "Cancelado pelo admin; valor devolvido ao saldo")
REVERSEDDelivered, then returned by the recipient (reversalReason, reversalAmount, reversalStatus). The returned amount is credited automatically only when the brand enables it — see Returned payouts

REJECTED is not only "admin denied": a create whose reserve fails is also marked REJECTED, with the funds returned.

Uncertain dispatch stays PROCESSING. When the provider call times out, the PIX may or may not have gone out. The payout (or leg) stays PROCESSING while Liqfy confirms with the provider — it is only failed and refunded with positive evidence that the PIX did not go out. When that check proves it, the failure message is "Saque não efetivado pelo provedor. O valor foi devolvido ao saldo."

#Response fields beyond the create body

GET /v1/withdrawals/{id}, the list and the cancel response add:

FieldWhat it is
legs[]{ id, legIndex, amount, status, endToEndId, failureCode, refunded, reversalCredited, reversalEndToEndId, reversedAmount, reversedAt, createdAt, updatedAt }. A PIX payout above the per-transaction cap is split into N legs at approval; legIndex is 1-based. Legs replaced by an internal re-dispatch are not listed.
legSummaryAggregate over legs: { count, paidOut, returned, awaitingReturn, pendingDecision, inFlight, total }.
recipientNameAccount holder name reported by the provider at settlement (prefers a completed leg), when available.
reversalReason, reversalAmount, reversalStatusPresent when the payout was reversed.

Leg fields

  • status — PENDING, PROCESSING, COMPLETED, FAILED or REVERSED.
  • endToEndId — the leg's own PIX E2E, to reconcile against bank statements.
  • failureCode — from a closed set, never provider text: INVALID_PIX_KEY, ACCOUNT_CLOSED, ACCOUNT_BLOCKED, KYC_REJECTED, LIMIT_REJECTED, DESTINATION_UNAVAILABLE, DESTINATION_NOT_ALLOWED, INSUFFICIENT_BALANCE, TIMEOUT, PROVIDER_CANCELLED, PROVIDER_REJECTED, UNKNOWN. null when the leg did not fail.
  • refunded — only for a FAILED leg: true once the refund credit actually exists in the ledger (leg refund or full payout refund). false means the refund is still held.
  • reversalCredited — only for a REVERSED leg: true once the returned amount was credited to your balance.
  • reversalEndToEndId, reversedAmount, reversedAt — details of the return, for a REVERSED leg.

legSummary

FieldSum of
paidOutCOMPLETED legs — what reached the destination.
returnedFAILED legs already refunded + REVERSED legs already credited. Leg amounts only: when the whole payout is refunded, the returned fee is not included here.
awaitingReturnFAILED legs whose refund has not landed yet.
pendingDecisionREVERSED legs whose returned amount has not been credited to you.
inFlightPENDING / PROCESSING legs.
totalAll legs — equals the payout's netAmount.

#Returned payouts

When a recipient returns a paid PIX, the leg becomes REVERSED once the return is confirmed in the provider's statement (only a return of the full leg amount is accepted). The payout becomes REVERSED when every active leg was returned; with some legs returned and others not, the payout keeps its status and the returned legs show REVERSED.

The returned money comes back to Liqfy, not straight to your wallet. It is credited to your balance without the fee automatically only when your brand enables it and the return is not flagged as fraud or suspicion (codes BE08, FR01); otherwise a human decides to credit or retain it. Watch legs[].reversalCredited and legSummary.pendingDecision.

reversalReason uses these labels for the BACEN return codes:

CodeLabel
BE08Devolução por suspeita de fraude
FR01Conta do recebedor sob suspeita de fraude
MD06Devolução solicitada pelo pagador
AC03Chave/conta do recebedor inválida
AC04Conta do recebedor encerrada
AC06Conta do recebedor bloqueada
AM09Valor incorreto
SL02Devolução por solicitação do participante
DUPLPagamento em duplicidade
otherDevolução PIX (<code>), or Devolução PIX without a code

#Cancel a pending payout

POST /v1/withdrawals/{id}/cancel — optional body { "reason": "..." } (trimmed, up to 500 characters; default "Cancelado pelo lojista."). Same auth as the create endpoint. Accepts the plain id or the po_ id.

  • Only while the payout is PENDING (nothing dispatched yet). It becomes CANCELLED, amount + fee go back to the wallet in the same transaction, and the outcome fires the webhooks below. Returns 200 with the payout (retrieve shape).
  • Already approved, dispatched or terminal → 409, code = WITHDRAWAL_NOT_CANCELLABLE. Rows that only our support team can cancel (source MANUAL, above the manual-approval floor, non-BRL) also return 409: cancel those through support.
  • In the dashboard, only the account owner or an admin can cancel (a regular member gets 403, code = WITHDRAWAL_CANCEL_ROLE_REQUIRED). Via the API, the account key is enough. The account receives a "Withdrawal cancelled" email.
  • Another account's payout → 404 — and the 404 never confirms the id exists.
bash
curl -X POST https://api.liqfy.com.br/v1/withdrawals/f6a7b8c9-d0e1-4234-9f01-234567890123/cancel \
  -H "Authorization: Bearer $LIQFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Duplicate request" }'
ts
await client.payouts.cancel('f6a7b8c9-d0e1-4234-9f01-234567890123', { reason: 'Duplicate request' });

Python client.payouts.cancel(id, reason=…), PHP $client->payouts->cancel($id, $reason), .NET client.Payouts.CancelAsync(id, reason).

#Receipt

GET /v1/withdrawals/{id}/receipt — not only for COMPLETED: the document exists for every terminal status (COMPLETED, PARTIALLY_COMPLETED, FAILED, REJECTED, CANCELLED, REVERSED). Non-terminal → 400. Optional format: pdf (default) or json; any other value → 400.

Response 200 OK

json
{
  "contentType": "application/pdf",
  "base64": "JVBERi0xLjQKJcTl8uXrp/Og0MTGCg=="
}

Only COMPLETED is titled "Comprovante de Transferência PIX"; the other documents record the outcome and state that they are not a payment proof:

StatusTitleAmount label
COMPLETEDComprovante de Transferência PIXValor transferido
PARTIALLY_COMPLETEDTransferência Parcialmente ConcluídaValor solicitado
FAILEDTransferência Não RealizadaValor solicitado
REJECTEDTransferência Não AprovadaValor solicitado
CANCELLEDTransferência CanceladaValor solicitado
REVERSEDTransferência DevolvidaValor da transferência

The document mentions that the amount went back to your balance only when that credit exists in the ledger — a cancelled payout without a refund never claims one. The BACEN E2E appears only on paid documents; the provider's transaction id never appears. CPF/CNPJ are masked; the destination PIX key is shown only when the receiver's name and document are missing. A reason line appears only for reasons recognised as customer-safe (invalid key, holder data mismatch, limit, destination account not eligible, cancellation, generic unavailability); raw provider messages never reach the document.

bash
curl https://api.liqfy.com.br/v1/withdrawals/f6a7b8c9-d0e1-4234-9f01-234567890123/receipt \
  -H "Authorization: Bearer $LIQFY_API_KEY"
ts
const receipt = await client.payouts.receipt('f6a7b8c9-d0e1-4234-9f01-234567890123');

Python client.payouts.receipt(id), PHP $client->payouts->receipt($id), .NET client.Payouts.ReceiptAsync(id).

#5. Webhooks

Subscribe to payout.created, payout.approved, payout.paid and payout.failed. All use the versioned evt_ envelope and timestamped signature. Approval means processing, before settlement. See Webhooks.

json
{
  "id": "evt_5f3a9b2c1d4e5f6a7b8c9d0e1f2a3b4c",
  "object": "event",
  "api_version": "2026-07-23",
  "type": "payout.paid",
  "created_at": "2026-04-25T16:11:42.000Z",
  "data": {
    "object": {
      "id": "po_f6a7b8c9-d0e1-4234-9f01-234567890123",
      "object": "payout",
      "status": "paid"
    }
  }
}

data.object.status uses the public vocabulary: pending, processing, paid, failed. FAILED, REJECTED and CANCELLED all arrive as payout.failed with status: "failed".

payout.failed carries data.object.failure_code and data.object.failure_message when available. Use the stable code to decide what to do. destination_not_allowed means the provider rejected this destination type; retry only with another Pix key type or destination. See Payout failures.

#6. List your payouts

GET /v1/withdrawals — same API key as the create endpoint.

QueryDefaultDescription
page1Page number (integer; anything else → 400).
limit20Integer. Hard-capped at 100 — a larger value is silently reduced, no error.
status—CSV of either vocabulary (below). Unknown token → 400 ("Invalid withdrawal status: …").
source—API, PANEL, MANUAL or INDEFINIDO (rows with no source). Any other value is silently ignored and the list is not filtered.
orderdescBy creation date. asc for oldest first; anything else is desc.
idempotency_key—Exact key, 1 to 255 characters after trimming (otherwise 400). Zero or one result.

Both vocabularies are accepted in status, and the wire values in the response are the internal uppercase ones. A lowercase public word expands to its set; any other token is matched against the internal status, case-insensitively (PROCESSING = only PROCESSING, while processing = APPROVED, PROCESSING and CONFIRMING; reversed = REVERSED).

Public filterInternal wire values
pendingPENDING
processingAPPROVED, PROCESSING, CONFIRMING
paidCOMPLETED, PARTIALLY_COMPLETED
failedFAILED, REJECTED, CANCELLED
(none)REVERSED
bash
curl "https://api.liqfy.com.br/v1/withdrawals?page=1&limit=20&status=paid&source=API&order=desc" \
  -H "Authorization: Bearer $LIQFY_API_KEY"
ts
const page = await client.payouts.list({ limit: 20, status: 'paid', page: 1, source: 'API', order: 'desc' });

Python client.payouts.list(limit=…, status=…, page=…, source=…, order=…), PHP $client->payouts->list([...]), .NET client.Payouts.ListAsync(...).

Response 200 OK

json
{
  "data": [
    {
      "id": "f6a7b8c9-d0e1-4234-9f01-234567890123",
      "type": "PIX",
      "status": "COMPLETED",
      "amount": 50000,
      "fee": 100,
      "netAmount": 50000,
      "currency": "BRL",
      "source": "API",
      "completedAt": "2026-04-25T16:11:42.000Z",
      "recipientName": "Maria Silva",
      "legs": [
        {
          "id": "c1d2e3f4-0000-0000-0000-000000000001",
          "legIndex": 1,
          "amount": 50000,
          "status": "COMPLETED",
          "endToEndId": "E12345678202604251611abcdef123456",
          "failureCode": null,
          "refunded": false,
          "reversalCredited": false
        }
      ],
      "legSummary": { "count": 1, "paidOut": 50000, "returned": 0, "awaitingReturn": 0, "pendingDecision": 0, "inFlight": 0, "total": 50000 },
      "...": "..."
    }
  ],
  "total": 17,
  "page": 1,
  "limit": 20
}

#7. Limits & rules

  • Per-transaction PIX cap — R$ 15.000,00 by default (a platform setting). Above the cap the payout is split into N legs at approval: each leg is filled up to the cap and the remainder goes on the last one (R$ 110.000,00 → 7 × R$ 15.000,00 + 1 × R$ 5.000,00). Each leg is its own PIX, with its own E2E, and can fail independently.
  • Per-account withdrawal bounds — minWithdrawalAmount / maxWithdrawalAmount per request, dailyPayoutMaxAmount / dailyPayoutMaxCount per day (null = no limit). Out of bounds answers AMOUNT_BELOW_MINIMUM, AMOUNT_ABOVE_MAXIMUM or DAILY_PAYOUT_LIMIT_EXCEEDED.
  • Wallet must cover amount + fee — partial debits never happen; the request is rejected outright.
  • Approval — payouts at or above manualApprovalThresholdCents (default R$ 15.001,00 — one cent above the PIX cap, so any single-leg PIX up to the cap can auto-approve), every non-BRL payout and rows sent to manual review need approval. Auto-payout is account policy (autoPayoutEnabled, autoWithdrawPct — balance-percentage gate, default 50, eligible payout types, the automatic payout limit and whether all payouts require manual approval) — ask support to review yours. An eligible payout is dispatched in seconds at create; a sweep every 5 minutes re-checks payouts still PENDING against the same policy.
  • Velocity brake — an unusual burst of payouts from the account, or several accounts paying the same PIX key in a short window, can send a payout to manual approval instead of auto-payout. It never rejects the payout.
  • Withdrawal rules — platform rules can refuse a payout (the refusal carries ruleId and reasonKey: withdrawal.rule.pixKeyTypeNotAllowed or withdrawal.rule.notAllowed) or require approval for it.
  • TED — restricted to Brazilian banks (FEBRABAN bankCode list). Settlement: same business day if approved before 16:30 BRT.

#Error codes

Branch on error.code. The message is for your logs; the code is the contract. Exception: some tokens (WALLET_NOT_FOUND, WALLET_NOT_SPENDABLE, TENANT_KYC_REQUIRED, CRYPTO_PANEL_ONLY) are plain message text — their rows show the fallback error.code, and you match the token in error.message. Full map: Errors.

HTTPerror.codeWhen
400INSUFFICIENT_BALANCENo wallet covers the debit (details: available, requested, currency). 400 on this path — not 422.
400invalid_requestThe explicit walletId is unknown, inactive, of another currency or not spendable. Token in error.message: WALLET_NOT_FOUND / WALLET_NOT_SPENDABLE.
400invalid_requestBody validation failed, Idempotency-Key longer than 255 characters, invalid status filter or invalid idempotency key on lookup.
400AMOUNT_BELOW_MINIMUM / AMOUNT_ABOVE_MAXIMUMOutside the account's withdrawal bounds.
422DAILY_PAYOUT_LIMIT_EXCEEDEDThe account's daily amount or count cap was reached.
403account_suspendedAccount suspended or blocked.
403forbiddenAccount KYC is not active (when the KYC gate is enforced). Token in error.message: TENANT_KYC_REQUIRED.
403forbiddentype: "CRYPTO" (or the crypto estimate) called with an API key. Token in error.message: CRYPTO_PANEL_ONLY.
403WITHDRAWAL_CANCEL_ROLE_REQUIREDThe account membership is not OWNER/ADMIN: withdrawal creation or cancellation, wallet transfer, or saved withdrawal destination creation, update or deletion. API-key fiat payouts keep their existing access.
404not_foundUnknown id, or a payout of another account — the 404 never confirms the id exists.
409WITHDRAWAL_NOT_CANCELLABLECancel of a non-PENDING payout, or a row only support can cancel.
409(no stable code)Keyless create near-duplicate within 90 s. Send an Idempotency-Key and retry.

#FAQ

Q: I created a payout but it's stuck on PENDING. A: It is waiting for approval — immediate dispatch only covers what your account policy allows. Common reasons: auto-payout is off for the account, the type is not auto-paid (by default only PIX), the amount is above the auto-payout ceiling or at/above manualApprovalThresholdCents (default R$ 15.001,00), the balance gate (autoWithdrawPct), the velocity brake or a rule requiring approval. Approve via the dashboard or ask support about auto-payout.

Q: My payout FAILED — was I charged the fee? A: No. FAILED triggers an automatic, atomic wallet refund of amount + fee. The refund lands as soon as the wallet movement happens; when failure_message is "Estamos confirmando com o banco; você será avisado.", the refund is held (legs[].refunded is false) — the money is not back yet, wait for the credit and notice.

Q: My payout was REVERSED — is the money back in my balance? A: Not necessarily. The returned amount is credited without the fee automatically only when your brand enables it and the return is not flagged as fraud; otherwise it waits for a decision. Check legs[].reversalCredited and legSummary.pendingDecision.

Q: Can I cancel a payout? A: Only while PENDING, with POST /v1/withdrawals/{id}/cancel or the Cancel withdrawal button in the dashboard. Once approved, the funds are in flight.

Q: My CNPJ key was rejected as invalid even though my bank accepts it. A: The mod-11 checksum is enforced and there is no bypass parameter. If your key fails the check, contact support.

Q: My phone key was validated as a CPF. A: Without +55, an 11-digit phone looks like a CPF to auto-detection. Send the phone as +55… or declare type: "PHONE".

Q: TED bank not in my dropdown. A: We use the Bacen FEBRABAN code list. Open a ticket if a code is missing — usually fixed within 24h.

#Settlement identifiers and lookup

A charge.paid or payout.paid may include data.object.settlement.end_to_end_id only when a valid BACEN Pix E2E is available. Provider transaction IDs are not E2Es. For multi-leg payouts, a single top-level identifier is not a substitute for the individual legs; the JSON receipt exposes valid E2Es on completed legs when multiple active legs exist. Absence does not mean the payment failed.

Paid-webhook retention is opt-in per merchant. Strict retention prevents delivery without an E2E but can hold a webhook indefinitely if the provider never supplies it; the wallet credit does not wait. Restrict routing to verified E2E-capable routes for a practical guarantee. A timed release may deliver without E2E, so it is not the strict guarantee.

Retrieve resources created by your integration with GET /v1/charges/by-idempotency-key/{key} or GET /v1/withdrawals/by-idempotency-key/{key}; percent-encode the key. For /, ?, #, or spaces, use the list query ?idempotency_key= with URL encoding instead. Lists contain zero or one match within your account. Charge keys are required; send a key for every payout because payouts without one cannot be found this way. A payout rejected during reservation releases its key and will not be found by it.

Payout retrieval, cancellation and receipts accept po_ IDs from webhooks. GET /v1/withdrawals/{id}/receipt?format=json returns structured data for all terminal outcomes: completed, partially completed, failed, rejected, cancelled and reversed. Only completed is a payment proof. Non-terminal payouts return 400. The default remains the PDF envelope {contentType, base64}; Accept does not choose the format. Documents are masked, and the Pix key appears only when receiver name and document are absent.

#Lookup examples

The examples below use your API origin in API_URL and your server-side key in API_KEY. A key must contain 1–255 characters after trimming; an empty or longer key returns 400. The path lookup returns a neutral 404 when no resource belongs to your account, including when another account used the same key. The list filter returns the normal list envelope with zero or one item.

bash
# API_URL is the API origin; API_KEY is your server-side key.
curl "$API_URL/v1/charges/by-idempotency-key/order-42" \
  -H "Authorization: Bearer $API_KEY"
curl "$API_URL/v1/withdrawals/by-idempotency-key/order-42" \
  -H "Authorization: Bearer $API_KEY"

# URL encoding is especially useful for keys containing /, ?, #, or spaces.
curl --get "$API_URL/v1/charges" \
  -H "Authorization: Bearer $API_KEY" \
  --data-urlencode "idempotency_key=order/42 ?#"
curl --get "$API_URL/v1/withdrawals" \
  -H "Authorization: Bearer $API_KEY" \
  --data-urlencode "idempotency_key=order/42 ?#"

curl "$API_URL/v1/withdrawals/po_f6a7b8c9-d0e1-4234-9f01-234567890123/receipt?format=json" \
  -H "Authorization: Bearer $API_KEY"

#JSON receipt contract

The response below illustrates a completed Pix payout. Amounts are in the currency’s smallest unit (150000 means R$1,500.00). Receipt text is in Portuguese, as in the PDF; issuer and the footer depend on the brand and outcome.

json
{
  "object": "payout_receipt",
  "payout_id": "po_f6a7b8c9-d0e1-4234-9f01-234567890123",
  "status": "paid",
  "outcome": "completed",
  "is_payment_proof": true,
  "title": "Comprovante de Transferência PIX",
  "amount": 150000,
  "amount_label": "Valor transferido",
  "currency": "BRL",
  "transferred_at": "2026-10-28T14:33:02.000Z",
  "end_to_end_id": "E12345678202610281433Zx9Yw8Vu7Ts",
  "payer": { "name": "Example Store", "document": "12.345.678/****-**" },
  "receiver": { "name": "Maria da Silva", "document": "***.456.789-**", "pix_key": null },
  "refunded_to_balance": false,
  "what_happened": "A transferência foi concluída com sucesso.",
  "what_to_do": null,
  "reason": null,
  "legs": null,
  "issuer": "Example Brand",
  "footer": "O identificador E2E acima é registrado no Banco Central do Brasil e pode ser usado para confirmar esta transferência em qualquer instituição financeira participante do PIX.",
  "issued_at": "2026-10-28T15:00:00.000Z"
}
  • status is paid for the completed and partially_completed outcomes and failed for every other outcome — including reversed. outcome identifies the terminal outcome. In particular, a partially_completed outcome has status paid but is_payment_proof is false.
  • title and amount_label follow the outcome table in Receipt.
  • For outcomes other than completed, transferred_at and the top-level end_to_end_id are null. Even for a completed payout, end_to_end_id is null unless a valid Pix E2E is available. what_to_do and reason can be null; reason is a customer-safe explanation, not a raw provider error.
  • footer: with an E2E, the BACEN text above; paid without E2E, "Comprovante emitido pela Liqfy com base nos registros da transferência."; otherwise "Documento emitido pela Liqfy com base nos registros desta solicitação. NÃO é comprovante de pagamento."
  • refunded_to_balance reflects an actual ledger credit; it is not inferred from the status. payer and receiver names or documents can be null; CPF/CNPJ documents are masked. receiver.pix_key is populated only when both receiver name and document are absent.
  • legs is null when there are zero or one active legs. With multiple active legs it contains their index, amount, lower-case status and E2E. Superseded legs are excluded. Only completed legs can contain a valid E2E, including for a partially completed payout; other leg E2Es are null. For example, a partially completed payout can contain:
json
{
  "legs": [
  { "index": 0, "amount": 60000, "status": "completed", "end_to_end_id": "E12345678202610281433Zx9Yw8Vu7Ts" },
  { "index": 1, "amount": 90000, "status": "failed", "end_to_end_id": null }
  ]
}

#SDK examples

These examples assume client is already configured, payoutId / payout_id is the payout ID, and key is the original idempotency key.

Node.js

ts
const receipt = await client.payouts.receipt(payoutId, { format: 'json' });
const charge = await client.charges.retrieveByIdempotencyKey(key);
const payout = await client.payouts.retrieveByIdempotencyKey(key);
const charges = await client.charges.list({ idempotency_key: key });
const payouts = await client.payouts.list({ idempotency_key: key });

Python

python
receipt = client.payouts.receipt(payout_id, format="json")
charge = client.charges.retrieve_by_idempotency_key(key)
payout = client.payouts.retrieve_by_idempotency_key(key)
charges = client.charges.list(idempotency_key=key)
payouts = client.payouts.list(idempotency_key=key)

PHP

php
$receipt = $client->payouts->receipt($payoutId, ['format' => 'json']);
$charge = $client->charges->retrieveByIdempotencyKey($key);
$payout = $client->payouts->retrieveByIdempotencyKey($key);
$charges = $client->charges->list(['idempotency_key' => $key]);
$payouts = $client->payouts->list(['idempotency_key' => $key]);

.NET

csharp
var receipt = await client.Payouts.ReceiptJsonAsync(payoutId);
var charge = await client.Charges.RetrieveByIdempotencyKeyAsync(key);
var payout = await client.Payouts.RetrieveByIdempotencyKeyAsync(key);
var charges = await client.Charges.ListByIdempotencyKeyAsync(key);
var payouts = await client.Payouts.ListByIdempotencyKeyAsync(key);