Saque (payout) — o que a Liqfy responde quando não dá certo
Este documento fecha o contrato de falha de saque: o que você recebe, quando, e o que fazer com cada caso. Serve para você implementar uma vez e não precisar mexer de novo.
Há dois momentos em que um saque pode não acontecer, e eles se comportam de forma bem diferente. Não trate os dois no mesmo caminho de código. Um terceiro caso é mais raro: o saque pago e depois devolvido pela instituição de destino (veja a seção 4).
#1. Recusa imediata — o saldo da SUA conta não cobre
Acontece na hora, na resposta do POST /v1/withdrawals. Nenhum saque é
criado, nenhum valor sai da sua carteira, e o idempotencyKey NÃO é gasto.
POST /v1/withdrawals
HTTP/1.1 400 Bad Request
{
"error": {
"type": "invalid_request_error",
"code": "INSUFFICIENT_BALANCE",
"message": "INSUFFICIENT_BALANCE",
"details": { "available": 1000, "requested": 100005, "currency": "BRL" }
},
"request_id": "req_..."
}Ramifique em error.code. A message é para log; o código é o contrato.
| Campo | O que é |
|---|---|
error.code | Sempre INSUFFICIENT_BALANCE neste caso. Constante estável. |
details.available | Saldo gastável, na menor unidade (centavos no BRL). |
details.requested | O que sairia da carteira: amount + taxa. Não é o amount que você mandou |
details.currency | ISO 4217 da carteira avaliada. |
#Três coisas que evitam suporte depois
A taxa é cobrada POR CIMA. Para sacar amount, a carteira precisa de
amount + taxa (o amountType padrão é "gross"). Por isso requested vem
somado — sem ele você veria "pedi 1000, tenho 1000" e a recusa pareceria
errada. (O modo legado amountType: "net" ainda é aceito no
POST /v1/withdrawals — veja Criar o saque: o recebedor
recebe amount − taxa, e aí requested é só o amount. Ele não é aceito em
saque CRYPTO.)
Só available conta. É o saldo gastável. O pending (liquidado mas ainda
retido) não entra na conta e nunca cobre um saque. Se a conta tiver mais de
uma carteira na mesma moeda, available é o maior saldo entre elas, não a
soma — a reserva sai de uma carteira só. Você pode fixar a carteira com o campo
opcional walletId; sem ele, vale a carteira padrão da moeda quando tem saldo,
senão qualquer carteira gastável com saldo suficiente.
A recusa não queima o idempotencyKey. Você pode depositar e reenviar com
a MESMA chave; o saque é processado normalmente. (Isso valia para a recusa
comum e passou a valer também para a corrida rara em que o saldo some entre a
checagem e a reserva — corrigido em 19/08/2026.)
#Como não bater neste erro
GET /v1/withdrawals/payout-info?currency= — sem efeito colateral, feito
para isso. Devolve { currency, available, pendingAdjustment, fee: { fixed, variablePct, minimum }, maxWithdrawable }: available é o saldo gastável,
fee é o conjunto de parâmetros aplicado hoje e maxWithdrawable é o
maior valor bruto que você pode pedir com a taxa já descontada — o número
certo para o botão "sacar tudo". Contrato completo e exemplo:
Cotação, taxa e saldo sacável.
#Outras recusas imediatas
A mesma regra vale para todo 4xx síncrono: nada é criado e nada sai da carteira.
| HTTP | Código / mensagem | Quando |
|---|---|---|
| 400 | erros de validação | O corpo não passa no esquema (ex.: type fora de PIX, TED, CRYPTO; amount que não é inteiro positivo). |
| 400 | Idempotency-Key must be at most 255 characters | O cabeçalho Idempotency-Key tem mais de 255 caracteres. |
| 4xx | TENANT_KYC_REQUIRED | O cadastro (KYC) da conta não está aprovado. |
| 4xx | recusa por regra de saque | Uma regra de saque configurada para a conta recusou o pedido. A recusa pode trazer ruleId e reasonKey (withdrawal.rule.pixKeyTypeNotAllowed ou withdrawal.rule.notAllowed). |
| 403 | CRYPTO_PANEL_ONLY | Saque CRYPTO enviado com chave de API — cripto é só pelo painel. |
| 400 | INVALID_CRYPTO_WITHDRAWAL | Saque CRYPTO fora das regras (só BRL, mínimo R$ 5.000,00, sem net, endereço de carteira e cotação obrigatórios). |
Toda recusa 4xx deixa rastro. Ela é registrada com valor, motivo e origem
(API ou painel) e aparece na tela de saques e no log de auditoria
(WITHDRAWAL_REJECTED). O mesmo pedido repetido com a mesma chave de
idempotência e o mesmo código é registrado no máximo uma vez por hora. Um 5xx é
falha nossa, não recusa, e não entra nesse registro.
#2. Recusa depois — o saque foi aceito e falhou no processamento
Aqui o POST respondeu 201 com status: "PENDING", e o valor já saiu da
sua carteira (fica reservado). O desfecho chega depois, de duas formas:
- pelo webhook
payout.failed; - ou consultando
GET /v1/withdrawals/{id}(po_...ou o id puro). A listaGET /v1/withdrawals?status=failedaceita as mesmas palavras minúsculas dos webhookspayout.*:pending,processing,paid,failed(failed=FAILED,REJECTEDeCANCELLED). Valor desconhecido devolve 400.
Os campos que interessam:
| Campo | O que é |
|---|---|
status | FAILED, REJECTED ou CANCELLED (failed no payout canônico) |
failure_code | Código estável, minúsculo, legível por máquina. É por ele que você decide o fluxo. Vem quando o saque é FAILED. |
failure_message | Frase neutra em português para aquele código. Pode ir para o seu usuário. |
rejectionReason | Texto livre para log/suporte (reason no webhook legado). No FAILED o valor é a failure_message pública (desde 2026-10-21); em REJECTED/CANCELLED ainda pode trazer o texto técnico da instituição de destino. Não faça if em cima dele. |
legs[].failureCode | Só em saque dividido em partes: o motivo de cada parte. Vocabulário fechado: INVALID_PIX_KEY, ACCOUNT_CLOSED, ACCOUNT_BLOCKED, KYC_REJECTED, LIMIT_REJECTED, DESTINATION_UNAVAILABLE, DESTINATION_NOT_ALLOWED, INSUFFICIENT_BALANCE, TIMEOUT, PROVIDER_CANCELLED, PROVIDER_REJECTED, UNKNOWN. |
Onde cada um aparece:
| Onde | failure_code / failure_message | Texto livre |
|---|---|---|
GET /v1/withdrawals/{id} (e a lista) | failure_code, failure_message na raiz | rejectionReason |
payout.failed | data.object.failure_code, data.object.failure_message | nunca |
{
"id": "evt_...",
"object": "event",
"type": "payout.failed",
"data": {
"object": {
"id": "po_...",
"object": "payout",
"status": "failed",
"failure_code": "destination_unavailable",
"failure_message": "O banco de destino não confirmou o PIX a tempo. Tente novamente mais tarde."
}
}
}#Valores de failure_code
failure_code | failure_message | Vale tentar de novo? |
|---|---|---|
invalid_pix_key | A chave PIX de destino é inválida ou não foi encontrada. | Só com outra chave |
account_blocked | A conta de destino está bloqueada para receber PIX. | Só com outra conta |
account_closed | A conta de destino está encerrada ou inativa. | Só com outra conta |
limit_rejected | O valor excede um limite permitido para esta transferência. | Sim, com valor menor |
kyc_rejected | A transferência foi recusada na verificação cadastral do destinatário. | Não |
destination_unavailable | O banco de destino não confirmou o PIX a tempo. Tente novamente mais tarde. | Sim, mais tarde: o banco de destino não liquidou a tempo |
destination_not_allowed | O provedor não aceita esse tipo de destino; use outra chave Pix. | Só com outro tipo de chave ou destino |
payout_cancelled | A transferência foi cancelada antes de ser concluída. | Sim |
provider_rejected | A transferência PIX foi recusada e não foi concluída. | Sim, mas investigue antes |
provider_rejected é o código genérico: o que não dá para atribuir com mais
precisão cai nele. Trate um failure_code desconhecido como
provider_rejected — códigos novos podem ser acrescentados; nenhum é
renomeado ou removido.
Regra geral: um saque FAILED é definitivo e o valor volta
automaticamente para a sua carteira; você não precisa pedir estorno. Quando o
payout.failed chega depende de uma configuração da conta:
- Só falhas definitivas (recomendado se você reembolsa o seu usuário ao
receber
payout.failed): todopayout.failedchega com o estorno já lançado, então você pode reembolsar o usuário na hora. Se o estorno precisar de confirmação com o banco, opayout.failedespera até ele ser lançado; se o PIX saiu, chegapayout.paidno lugar. Peça ao suporte para ativar. - Padrão: o
payout.failedé enviado na hora. Quase sempre o estorno já foi lançado. Quando não dá para provar que o PIX nunca saiu, o estorno fica retido para confirmação com o banco: afailure_messageé "Estamos confirmando com o banco; você será avisado." elegs[].refundedéfalse. Não reembolse o usuário ainda: consulteGET /v1/withdrawals/{id}ou o extrato da carteira até o estorno cair. A liberação não gera outro webhook.
(A failure_message não promete estorno — quem prova é o lançamento na carteira.)
#Saque dividido em partes (pernas)
Cada PIX tem um teto (R$ 15.000,00 por padrão). Um saque acima dele é dividido em várias transferências PIX — as pernas —, cada uma cheia até o teto e o resto na última (R$ 110.000,00 → 7 × R$ 15.000,00 + 1 × R$ 5.000,00). Cada perna tem o seu próprio E2E e pode falhar sozinha.
Quando parte das pernas é paga e parte falha, o saque termina
PARTIALLY_COMPLETED — paid no vocabulário público — e o valor das pernas que
falharam volta para a sua carteira assim que o estorno cai. Olhe legs[] para
saber exatamente o que aconteceu:
| Campo | O que é |
|---|---|
legIndex | Número da perna, começando em 1. |
amount | Valor da perna, em centavos. |
status | PENDING, PROCESSING, COMPLETED, FAILED ou REVERSED. |
endToEndId | O E2E da perna, para conciliar com o extrato. |
failureCode | Vocabulário fechado acima; null quando a perna não falhou. |
refunded | Só em perna FAILED: true quando o estorno dela já caiu na carteira. |
reversalCredited | Só em perna REVERSED: true quando o valor devolvido foi creditado para você (seção 4). |
reversalEndToEndId, reversedAmount, reversedAt | Dados da devolução, em perna REVERSED. |
Uma perna substituída por um reenvio nunca aparece em legs[]; o valor dela já
está na perna que a substituiu.
#Cancelar um saque ainda PENDING
Enquanto o saque está PENDING (nada enviado ainda — por exemplo, aguardando
aprovação), você mesmo pode cancelá-lo:
POST /v1/withdrawals/{id}/cancel
{ "reason": "opcional, até 500 caracteres" }Responde 200 com o saque CANCELLED, e o valor volta para o saldo. Fora de
PENDING responde 409 WITHDRAWAL_NOT_CANCELLABLE. Pelo painel, só o dono
ou um admin da conta pode cancelar.
#3. Liquidez do processador
Quando o processador que faria o PIX está sem saldo para pagar, isso não é
culpa sua. Hoje o saque chega para você como FAILED com
failure_code: "provider_rejected", e o valor volta automaticamente para a
sua carteira assim que o lançamento na carteira cai (o estorno retido também
vale aqui — veja a regra geral acima). Não há retentativa automática nem janela
de espera: se quiser, peça o saque de novo — de preferência um pouco depois.
O que fazer: enquanto o saque estiver PROCESSING, não repita o pedido.
Um novo POST criaria um segundo saque e debitaria a carteira de novo. Mostre ao
seu usuário algo como "pagamento em processamento" e aguarde o webhook final.
O PROCESSING pode durar mais que o normal quando o resultado do envio do PIX
ainda não é conhecido. Nesse caso o saque segue PROCESSING enquanto a
Liqfy confirma o desfecho com o provedor: ele nunca falha e é
estornado só porque o tempo passou — só com prova positiva de que o PIX não saiu.
#4. Pago e depois devolvido (REVERSED)
Um PIX já pago pode ser devolvido pela instituição de destino. O saque (ou a
perna) passa a REVERSED. Esse status não tem palavra no vocabulário público
dos payout.*: para listá-lo, filtre com ?status=REVERSED.
O dinheiro devolvido não vai direto para o seu saldo. Depois que a devolução
é confirmada no extrato do provedor, ele entra numa caixa de reversões da
Liqfy e só é creditado para você automaticamente quando a política da
marca permite e a devolução não tem sinal de fraude ou suspeita; caso contrário,
uma pessoa decide — creditar para você ou reter. Até lá,
legs[].reversalCredited é false: trate como "ainda não voltou para o saldo".
#Resumo para quem vai implementar
POST /v1/withdrawals
├── 400 INSUFFICIENT_BALANCE ....... sua carteira não cobre. Nada foi criado.
│ → cheque /v1/withdrawals/payout-info antes
├── outros 4xx ..................... recusado na entrada. Nada foi criado.
└── 201 PENDING .................... aceito, valor reservado
├── ainda PENDING .............. dá para cancelar: POST /v1/withdrawals/{id}/cancel
├── segue PROCESSING ........... NÃO repita. Aguarde o webhook.
├── PARTIALLY_COMPLETED (paid) . parte das pernas paga, as que falharam
│ voltam (legs[].refunded diz quais já caíram)
├── REVERSED ................... pago e devolvido; crédito só quando
│ legs[].reversalCredited = true
└── webhook payout.failed
└── failure_code = invalid_pix_key | account_* | limit_rejected |
kyc_rejected | destination_unavailable | destination_not_allowed |
payout_cancelled |
provider_rejected (genérico / desconhecido)
→ definitivo; o estorno é automático assim que o lançamento
na carteira cai. Se a failure_message for "Estamos confirmando
com o banco; você será avisado." o estorno está retido
(legs[].refunded = false): consulte o saque ou o extrato
até cair. Não acontece com "só falhas definitivas".
Repetir só depois de corrigir a causa (ex.: chave PIX), ou mais
tarde no destination_unavailable.Idempotência: mande sempre o cabeçalho Idempotency-Key no
POST /v1/withdrawals (o campo idempotencyKey no corpo é fallback; o
cabeçalho prevalece; no máximo 255 caracteres). É a sua proteção contra saque
duplicado em qualquer retentativa de rede — repetir a mesma chave devolve o
saque original em vez de criar outro. Se uma requisição deu timeout e você não
sabe se o saque foi criado, consulte pela chave:
GET /v1/withdrawals/by-idempotency-key/{key} ou
GET /v1/withdrawals?idempotency_key={key}.