Saque no nero_payments¶
Para: Dev — Conferido em 2026-10-18.
Como implementar POST /api/wallet/withdraw/request e GET /api/payments/withdrawals/{withdrawal_id} sobre o POST /v1/withdrawals do Dracma. A referência da API está em Saques.
O que é¶
A rota do portal recebe o pedido legado do front, reserva o saldo, grava o intent e chama o Dracma. As particularidades da Nero são o corpo legado (pix_type e pix_key), as mensagens genéricas de bloqueio e a pré-checagem local que só endurece.
Quando usar¶
Ao implementar ou revisar a rota de saque do nero_payments.
Como funciona¶
Reserva antes do pedido¶
| O que chegou | Reserva | Por quê |
|---|---|---|
201 (qualquer estado) |
Mantém | O saque existe; o desfecho vem por webhook |
400 ou 422 |
Devolve | 4xx não cria objeto |
409 duplicate_external_id |
Mantém; segue pelo existing |
O saque já existe |
409 idempotency_key_reused |
Mantém; needs_decision e alerta (D12) |
Bug do conector; ninguém devolve nem reenvia sozinho |
Timeout, 5xx, 429, 409 idempotency_in_progress |
Mantém; repete com a mesma chave | Você não sabe se o saque foi criado |
| Tentativas esgotadas | Mantém; GET /v1/withdrawals?external_id=; inconclusivo vira needs_decision |
Nunca decida às cegas |
Por que nunca devolver no timeout: se o POST foi aceito e a resposta se perdeu, o jogador receberia o PIX e o saldo de volta.
Do corpo legado ao corpo do Dracma¶
Pedido do front (legado): {amount, type: "pix", pix_key, pix_type, accept_terms, currency?}.
| Campo legado | Vai ao Dracma como | Regra |
|---|---|---|
amount (reais) |
amount.amount na menor unidade, string |
A mesma conversão sem float do Depósito |
currency |
amount.currency |
Ausente vale BRL; outra moeda, recuse |
type |
method |
Só pix |
pix_type |
destination.pix.key_type |
cpf, cnpj, email, phone, random. Mapeie legados: evp e aleatoria para random; telefone e celular para phone |
pix_key |
destination.pix.key |
Normalize: CPF e CNPJ só dígitos; telefone em +55DDDNUMERO; e-mail em minúsculas; 1 a 140 caracteres |
accept_terms |
Não vai | Regra da bet: exija true antes de reservar |
| Nenhum | player.external_ref, document, name |
Do cadastro (asserção do portal) |
| Nenhum | player.ip |
IP do jogador vindo do Caddy e do portal. Sem ele o saque nunca é automático (OWNERSHIP_UNVERIFIED) |
| Nenhum | player.device_id |
Se a bet tiver |
| Nenhum | review_requested |
true quando a pré-checagem local desconfiar |
| Nenhum | player_context |
Opcional e informativo |
{
"external_id": "wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c",
"player": {
"external_ref": "0192a3b4-0000-7000-8000-000000000001",
"document": { "type": "CPF", "number": "52998224725", "country": "BR" },
"name": "Jogador Teste",
"ip": "203.0.113.10"
},
"amount": { "amount": "5000", "currency": "BRL" },
"method": "pix",
"destination": { "pix": { "key_type": "cpf", "key": "52998224725" } },
"review_requested": false
}
A chave PIX tem de ser do jogador: chave cpf diferente do CPF do cadastro, ou cnpj para pessoa física, volta 422 document_mismatch. Chaves email, phone e random são conferidas na consulta de titularidade pelo Dracma, o que pode levar alguns minutos (requested).
Resposta do Dracma e resposta ao front¶
| Dracma | Reserva | Front (200 do legado) |
|---|---|---|
201 approved ou requested |
Mantém | {status: true, withdrawal_id, state: "processing", message: "Saque em processamento."} |
201 under_review |
Mantém | {status: true, withdrawal_id, state: "under_review", message: "Saque em análise."} |
Timeout, 5xx ou 429 (intent gravado) |
Mantém | {status: true, withdrawal_id, state: "processing", ...} |
422 amount_below_minimum ou amount_above_maximum |
Devolve | {status: false, message: <mensagem legada de limite>} |
422 document_invalid ou document_mismatch |
Devolve | {status: false, message: "A chave PIX precisa ser do titular da conta."} |
422 player_blocked, destination_blocked ou velocity_exceeded |
Devolve | {status: false, message: "Não foi possível solicitar o saque agora."} (genérica: não revele a lista) |
422 insufficient_operation_balance |
Devolve e alerta o financeiro | {status: false, message: "Saque temporariamente indisponível. Tente mais tarde."} |
400 validation_failed |
Devolve e alerta (bug do conector) | {status: false, message: <mensagem legada de dados inválidos>} |
400 currency_not_enabled ou method_not_enabled |
Devolve e alerta | {status: false, message: "Saque indisponível."} |
"Erros de negócio mantêm as mensagens atuais": use os textos que o front já mostra hoje; a tabela só sugere para os casos novos.
Pré-checagem local: só endurece¶
A Nero roda, antes do POST, uma checagem própria que conta também os depósitos anteriores ao Dracma (D3). Ela só pode pedir revisão:
- Se desconfiou, envia
review_requested: truee, se quiser,review_reasons: ["texto curto"](aparece no painel com o prefixoOP:). - Nunca "libera" nada: o Dracma decide o automático com os próprios números.
under_review e o desfecho¶
under_reviewé decidido no painel do Dracma por revisores da Nero (uma pessoa; duas quando o motivo éDUAL_APPROVAL_REQUIRED). A bet não tem rota de API para aprovar.- O desfecho chega por webhook:
approved,processing,paid(finaliza) ourejected(devolve). - Os motivos em
decision.reasonssão para o suporte, nunca para o jogador. Os mais comuns na Nero:FIRST_WITHDRAWAL(primeiro saque),NO_SETTLED_DEPOSITeAUTO_MULTIPLIER_EXCEEDED(X = 1),OWNERSHIP_UNVERIFIED(faltouplayer.ip),PIX_KEY_CHANGED(chave nova).
Cancelar, finalizar e devolver¶
POST /v1/withdrawals/{id}/cancel (chave determinística cnl_<external_id>) funciona em requested, under_review e em approved antes do envio. Depois, 409 invalid_state_transition: espere o desfecho. Só devolva a reserva ao ler cancelled no fetch-back.
No caminho do webhook, o efeito no nero_ledger (idempotente por external_id) é:
| Estado lido no fetch-back | Ação |
|---|---|
paid |
Finaliza a reserva; guarda o psp_reference (E2E id) |
rejected, failed, cancelled |
Devolve a reserva |
returned (depois de paid) ou recalled |
Nada automático: needs_decision e alerta (D7, D18) |
| Qualquer outro | Nada; a reserva continua |
-- devolução idempotente: no máximo uma por saque
INSERT INTO lancamento (origem, external_id, jogador_id, valor_minor, tipo)
VALUES ('dracma', $1, $2, $3, 'devolucao_reserva_saque')
ON CONFLICT (origem, external_id, tipo) DO NOTHING;
Exemplos¶
O primeiro saque de teste, com a aprovação simulada, está em Primeiros passos no sandbox.
Erros e casos de borda¶
- Saque sem
player.ip: vai sempre para revisão; verifique que o portal repassa o IP vindo do Caddy. - Chave PIX nova do jogador: o motivo
PIX_KEY_CHANGEDmanda para revisão; avise o jogador de que a análise pode demorar. - Muitos saques do mesmo jogador na hora:
429 rate_limited; mostre a mensagem genérica e não repita em laço.