Saque (payout) — o que a Neozentry QA 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.
#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. Por isso requested vem somado — sem ele você veria
"pedi 1000, tenho 1000" e a recusa pareceria errada. (Existe o modo legado
amountType: "net", em que o recebedor recebe amount − taxa; aí requested
é só o amount.)
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ó.
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 — sem efeito colateral, feito para isso.
Devolve o available, a taxa aplicável e o maxWithdrawable: o maior valor
que você pode pedir com a taxa já descontada. É o número certo para o botão
"sacar tudo".
#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
withdrawal.failed; - ou consultando
GET /v1/withdrawals/{id}.
Os campos que interessam:
| Campo | O que é |
|---|---|
status | FAILED, REJECTED ou CANCELLED |
providerErrorCode | Código estável, legível por máquina. É por ele que você decide o fluxo |
rejectionReason | Texto livre para log/suporte. Não faça if em cima dele |
#Códigos de providerErrorCode
| Código | Significado | Sua carteira | Vale tentar de novo? |
|---|---|---|---|
INVALID_PIX_KEY | Chave PIX inválida, inexistente ou bloqueada | Estornada | Só com outra chave |
ACCOUNT_CLOSED | Conta do recebedor encerrada | Estornada | Só com outra conta |
ACCOUNT_BLOCKED | Conta do recebedor bloqueada | Estornada | Só com outra conta |
KYC_REJECTED | Recebedor barrado na checagem do banco | Estornada | Não |
LIMIT_REJECTED | Estourou limite do arranjo/banco | Estornada | Nao |
PROVIDER_REJECTED | Recusa genérica do processador | Estornada | Sim, mas investigue antes |
PROVIDER_INSUFFICIENT_BALANCE | Liquidez do processador — ver seção 3 | NÃO estornada | Automático, não repita |
Regra geral: em todos os códigos acima, menos o último, a falha é definitiva e o valor volta automaticamente para a sua carteira. Você não precisa pedir estorno; basta reagir ao webhook.
#3. PROVIDER_INSUFFICIENT_BALANCE — a exceção, e a janela de 24h
Este código não é culpa sua e não é falha do seu saque. Ele significa que o processador que faria o PIX estava momentaneamente sem liquidez. O saque continua válido.
Por isso ele é o único código transitório da tabela, e se comporta diferente:
- o valor não é estornado — o saque fica retido, aguardando;
- a Neozentry QA re-tenta sozinha, sem você fazer nada;
- a janela é de 24 horas contadas a partir da primeira recusa;
- se o processador se recuperar dentro da janela, o saque segue normalmente e
você recebe
withdrawal.completed; - se as 24 horas passarem sem recuperação, aí sim ele vira
FAILEDcom estorno automático para a sua carteira, e você recebewithdrawal.failed.
O que fazer: ao ver este código, 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. Só existem dois
desfechos possíveis, e os dois chegam por webhook: completed ou failed.
Estado atual desta função. A retentativa de 24h está implementada e testada, mas ainda não ligada em produção — ela fica atrás de um interruptor que hoje está desligado. Enquanto estiver assim, uma recusa por liquidez do processador chega para você como
PROVIDER_REJECTED, com estorno imediato (o comportamento definitivo da tabela).Implemente o tratamento agora mesmo assim. O código
PROVIDER_INSUFFICIENT_BALANCEjá faz parte do contrato; no dia em que o interruptor for ligado, sua integração passa a receber o comportamento novo sem precisar de nenhuma alteração do seu lado. Avisaremos a data.
#Resumo para quem vai implementar
POST /v1/withdrawals
├── 400 INSUFFICIENT_BALANCE ....... sua carteira não cobre. Nada foi criado.
│ → cheque /v1/wallets/balance antes
└── 201 PENDING .................... aceito, valor reservado
└── webhook withdrawal.failed
├── providerErrorCode = PROVIDER_INSUFFICIENT_BALANCE
│ → NÃO repita. Retido, re-tentado por até 24h.
│ Desfecho vem por webhook (completed ou failed).
└── qualquer outro código
→ definitivo, carteira JÁ estornada.
Repetir só depois de corrigir a causa (ex.: chave PIX).Idempotência: mande sempre idempotencyKey no POST /v1/withdrawals. É a
sua proteção contra saque duplicado em qualquer retentativa de rede — repetir o
mesmo idempotencyKey devolve o saque original em vez de criar outro.