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
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 endpointPayouts 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).
#1. Validate a PIX key (recommended)
Before creating the payout, check the destination key is well-formed.
POST /v1/withdrawals/validate-pix-key
{
"key": "12345678909",
"type": "CPF"
}Response 201 Created
{
"isValid": true,
"type": "CPF",
"normalized": "12345678909"
}Supported type values
| Type | Format |
|---|---|
CPF | 11 digits (dots and dashes are stripped), mod-11 checksum. Numbers with all digits equal are rejected. |
CNPJ | 14 digits (dots, dashes and slashes are stripped), mod-11 checksum. Numbers with all digits equal are rejected. |
EMAIL | local@domain.tld shape, lowercased, max 77 chars. |
PHONE | +55 followed by 10 or 11 digits (+5511999999999). Spaces, dashes and parentheses are stripped. |
RANDOM | Random 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
{
"isValid": false,
"type": "CPF",
"normalized": "12345678900",
"error": "CPF checksum invalid"
}Possible error values
error | Type |
|---|---|
CPF must be 11 digits / CPF checksum invalid | CPF |
CNPJ must be 14 digits / CNPJ checksum invalid | CNPJ |
Invalid email format / Email exceeds 77 characters | EMAIL |
Phone must start with +55 / Phone must have 10 or 11 digits after +55 | PHONE |
Random key must be a valid UUID v4 | RANDOM |
Unable to detect PIX key type | auto-detection failed |
PIX key must not be empty | whitespace-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
{
"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.
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 }'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
| Query | Required | Description |
|---|---|---|
amount | yes | Integer, 1 to 10.000.000.000 (smallest unit). |
currency | no | Default BRL. 3 to 8 characters. |
type | no | PIX (default) or TED. |
Any value outside these bounds → 400.
Response 200 OK
{
"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.
curl "https://api.liqfy.com.br/v1/withdrawals/payout-fee?amount=50000¤cy=BRL&type=PIX" \
-H "Authorization: Bearer $LIQFY_API_KEY"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
{
"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).
curl "https://api.liqfy.com.br/v1/withdrawals/payout-info?currency=BRL" \
-H "Authorization: Bearer $LIQFY_API_KEY"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.
| Field | Type | Required | Description |
|---|---|---|---|
type | enum | yes | PIX or TED. (CRYPTO exists but is dashboard-only — see below.) |
amount | integer | yes | Positive integer, smallest currency unit. In the default gross mode it is what the recipient receives; the fee is debited on top. |
currency | string | no | Default BRL. |
pixKey | string | on PIX | Destination PIX key. |
pixKeyType | enum | no | CPF, CNPJ, EMAIL, PHONE, RANDOM. Omitted or unknown (for example EVP): ignored, key auto-detected. |
bankCode | string | on TED | Bank code (FEBRABAN). |
bankAgency | string | on TED | Branch (agência). |
bankAccount | string | on TED | Account number. |
walletId | string | no | Explicit 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. |
amountType | enum | no | gross (default) or net. net is legacy and still accepted: the debit is amount and the recipient gets amount − fee (fee >= amount → 400). |
idempotencyKey | string | no | Fallback — prefer the Idempotency-Key header. |
totpCode | string | no | Dashboard 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"(withcryptoWalletAddressIdandcryptoEstimateId) is accepted only from a logged-in dashboard user; with an API key it answers403with the tokenCRYPTO_PANEL_ONLY. Same forPOST /v1/withdrawals/crypto/estimate;GET /v1/withdrawals/crypto/availabilityanswers{ "enabled": false }to API keys. In the dashboard a crypto payout must be inBRL, from R$ 5.000,00 (500000), notnet, with both ids (otherwise400INVALID_CRYPTO_WITHDRAWAL), and always asks for the 2FA code.
Example — PIX payout
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)
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"
}'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
{
"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.codefor 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 gets403("Esta conta exige 2FA para saques…"); a missing or invalid code gets401("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
PENDING ─▶ APPROVED ─▶ PROCESSING ─▶ CONFIRMING ─▶ COMPLETED ✓ funds delivered
└▶ PARTIALLY_COMPLETED delivered in partsTerminal 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.
| Status | Wallet effect |
|---|---|
PENDING | Debited at create (amount + fee reserved) |
APPROVED | Reserved — approved, waiting dispatch |
PROCESSING | Reserved — sent for settlement |
CONFIRMING | Reserved — waiting the receiving institution's confirmation |
COMPLETED | Debited (terminal) — delivered |
PARTIALLY_COMPLETED | Debited (terminal) — some legs delivered, others failed (see legs); the failed legs are refunded |
FAILED | Auto-refunded — dispatch failed (failure_code). The refund can be held until confirmed (see Payout failures) |
REJECTED | Refunded — refused at review, or by the reserve (system:reserve-failed) |
CANCELLED | Refunded (amount + fee) — cancelled by you before dispatch, or when an admin deactivates the account (reason "Cancelado pelo admin; valor devolvido ao saldo") |
REVERSED | Delivered, 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:
| Field | What 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. |
legSummary | Aggregate over legs: { count, paidOut, returned, awaitingReturn, pendingDecision, inFlight, total }. |
recipientName | Account holder name reported by the provider at settlement (prefers a completed leg), when available. |
reversalReason, reversalAmount, reversalStatus | Present when the payout was reversed. |
Leg fields
status—PENDING,PROCESSING,COMPLETED,FAILEDorREVERSED.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.nullwhen the leg did not fail.refunded— only for aFAILEDleg:trueonce the refund credit actually exists in the ledger (leg refund or full payout refund).falsemeans the refund is still held.reversalCredited— only for aREVERSEDleg:trueonce the returned amount was credited to your balance.reversalEndToEndId,reversedAmount,reversedAt— details of the return, for aREVERSEDleg.
legSummary
| Field | Sum of |
|---|---|
paidOut | COMPLETED legs — what reached the destination. |
returned | FAILED legs already refunded + REVERSED legs already credited. Leg amounts only: when the whole payout is refunded, the returned fee is not included here. |
awaitingReturn | FAILED legs whose refund has not landed yet. |
pendingDecision | REVERSED legs whose returned amount has not been credited to you. |
inFlight | PENDING / PROCESSING legs. |
total | All 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:
| Code | Label |
|---|---|
BE08 | Devolução por suspeita de fraude |
FR01 | Conta do recebedor sob suspeita de fraude |
MD06 | Devolução solicitada pelo pagador |
AC03 | Chave/conta do recebedor inválida |
AC04 | Conta do recebedor encerrada |
AC06 | Conta do recebedor bloqueada |
AM09 | Valor incorreto |
SL02 | Devolução por solicitação do participante |
DUPL | Pagamento em duplicidade |
| other | Devoluçã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 becomesCANCELLED,amount + feego back to the wallet in the same transaction, and the outcome fires the webhooks below. Returns200with the payout (retrieve shape). - Already approved, dispatched or terminal →
409,code=WITHDRAWAL_NOT_CANCELLABLE. Rows that only our support team can cancel (sourceMANUAL, above the manual-approval floor, non-BRL) also return409: 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 the404never confirms the id exists.
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" }'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
{
"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:
| Status | Title | Amount label |
|---|---|---|
COMPLETED | Comprovante de Transferência PIX | Valor transferido |
PARTIALLY_COMPLETED | Transferência Parcialmente Concluída | Valor solicitado |
FAILED | Transferência Não Realizada | Valor solicitado |
REJECTED | Transferência Não Aprovada | Valor solicitado |
CANCELLED | Transferência Cancelada | Valor solicitado |
REVERSED | Transferência Devolvida | Valor 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.
curl https://api.liqfy.com.br/v1/withdrawals/f6a7b8c9-d0e1-4234-9f01-234567890123/receipt \
-H "Authorization: Bearer $LIQFY_API_KEY"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.
{
"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.
| Query | Default | Description |
|---|---|---|
page | 1 | Page number (integer; anything else → 400). |
limit | 20 | Integer. 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. |
order | desc | By 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 filter | Internal wire values |
|---|---|
pending | PENDING |
processing | APPROVED, PROCESSING, CONFIRMING |
paid | COMPLETED, PARTIALLY_COMPLETED |
failed | FAILED, REJECTED, CANCELLED |
| (none) | REVERSED |
curl "https://api.liqfy.com.br/v1/withdrawals?page=1&limit=20&status=paid&source=API&order=desc" \
-H "Authorization: Bearer $LIQFY_API_KEY"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
{
"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
legsat 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/maxWithdrawalAmountper request,dailyPayoutMaxAmount/dailyPayoutMaxCountper day (null = no limit). Out of bounds answersAMOUNT_BELOW_MINIMUM,AMOUNT_ABOVE_MAXIMUMorDAILY_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 stillPENDINGagainst 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
ruleIdandreasonKey:withdrawal.rule.pixKeyTypeNotAllowedorwithdrawal.rule.notAllowed) or require approval for it. - TED — restricted to Brazilian banks (FEBRABAN
bankCodelist). 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.
| HTTP | error.code | When |
|---|---|---|
| 400 | INSUFFICIENT_BALANCE | No wallet covers the debit (details: available, requested, currency). 400 on this path — not 422. |
| 400 | invalid_request | The explicit walletId is unknown, inactive, of another currency or not spendable. Token in error.message: WALLET_NOT_FOUND / WALLET_NOT_SPENDABLE. |
| 400 | invalid_request | Body validation failed, Idempotency-Key longer than 255 characters, invalid status filter or invalid idempotency key on lookup. |
| 400 | AMOUNT_BELOW_MINIMUM / AMOUNT_ABOVE_MAXIMUM | Outside the account's withdrawal bounds. |
| 422 | DAILY_PAYOUT_LIMIT_EXCEEDED | The account's daily amount or count cap was reached. |
| 403 | account_suspended | Account suspended or blocked. |
| 403 | forbidden | Account KYC is not active (when the KYC gate is enforced). Token in error.message: TENANT_KYC_REQUIRED. |
| 403 | forbidden | type: "CRYPTO" (or the crypto estimate) called with an API key. Token in error.message: CRYPTO_PANEL_ONLY. |
| 403 | WITHDRAWAL_CANCEL_ROLE_REQUIRED | The 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. |
| 404 | not_found | Unknown id, or a payout of another account — the 404 never confirms the id exists. |
| 409 | WITHDRAWAL_NOT_CANCELLABLE | Cancel 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.
# 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.
{
"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"
}statusispaidfor thecompletedandpartially_completedoutcomes andfailedfor every other outcome — includingreversed.outcomeidentifies the terminal outcome. In particular, apartially_completedoutcome has statuspaidbutis_payment_proofisfalse.titleandamount_labelfollow the outcome table in Receipt.- For outcomes other than
completed,transferred_atand the top-levelend_to_end_idarenull. Even for a completed payout,end_to_end_idisnullunless a valid Pix E2E is available.what_to_doandreasoncan benull;reasonis 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_balancereflects an actual ledger credit; it is not inferred from the status.payerandreceivernames or documents can benull; CPF/CNPJ documents are masked.receiver.pix_keyis populated only when both receiver name and document are absent.legsisnullwhen 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 arenull. For example, a partially completed payout can contain:
{
"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
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
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
$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
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);