Saques (cash-out PIX)¶
Estado. As rotas desta página estão implementadas no
dracma_api(conferido no código em 2026-09-27:internal/interface/http/v1/withdrawals/). Onde o código difere do Contrato v1 1.1.0, o texto diz o que o código faz hoje e aponta a divergência em divergencias.md.
Um saque é um pedido de pagamento ao jogador. O motor de regras do Dracma decide na hora se ele sai no automático ou vai para revisão humana, e o resultado final chega por webhook. O dinheiro do jogador já foi reservado por você antes do pedido.
Recorte de lançamento¶
| Método | Situação |
|---|---|
pix (BRL) |
disponível |
card, crypto, boleto, bank_transfer, wire, swift |
não disponível — 400 method_not_enabled |
valor fora do enum Metodo |
400 validation_failed (field: method, reason: enum) |
O PSP de saque é escolhido pelo Dracma por operação. O Mercado Pago não faz saque (não tem API pública de payout PIX, ADR-0013): numa operação que usa Mercado Pago para depósito, o saque sai por outro PSP (hoje a MaximaxPay, ADR-0003). Isso não muda nada na API.
Regra nº 1: reserve antes de pedir¶
Debite (reserve) o saldo do jogador ANTES de POST /withdrawals, na mesma transação em que você grava o
saque local (external_id, amount, destino). Por quê:
- o jogador não consegue apostar nem sacar de novo o mesmo dinheiro enquanto o saque está em voo;
- se a chamada falhar por rede ou 5xx, o dinheiro continua reservado — e você repete com a mesma
Idempotency-Key, sem risco de pagar duas vezes; - a reserva só volta ao jogador quando o Dracma disser, de forma conclusiva, que o saque não sai
(4xx síncrono, ou estado
rejected/failed/cancelledlido pela API).
Nunca devolva a reserva sem resposta conclusiva. Timeout, erro de rede, 5xx e 429 não são respostas
conclusivas: repita com a mesma chave ou consulte GET /withdrawals?external_id=<seu id>.
Convenções¶
- Dinheiro:
Money={"amount": "<inteiro na menor unidade>", "currency": "BRL"}; R$ 50,00 ="5000". Nunca float. external_idúnico por operação (sugestão:wdr_<uuidv7>),^[A-Za-z0-9_.:-]{1,64}$.player.external_refopaco e imutável, nunca e-mail/telefone.- Todo
POSTlevaIdempotency-Key(16–128[A-Za-z0-9_.:-]); recomendação:Idempotency-Key=external_id. - Pedidos assinados conforme autenticacao.md; valores de assinatura abaixo são ilustrativos.
Máquina de estados (Contrato §6.2, PIX — como o código implementa)¶
stateDiagram-v2
[*] --> requested
requested --> approved: regras → automático
requested --> under_review: regras → manual (ou review_requested, sem player.ip, DICT fora)
requested --> rejected: titular da chave diverge (DICT)
requested --> cancelled
under_review --> approved: aprovado (1 ou 2 pessoas)
under_review --> rejected
under_review --> cancelled
approved --> processing: 1º retorno do PSP (enviado)
approved --> cancelled: só antes de o comando de envio sair
processing --> paid
processing --> failed
paid --> returned: banco devolveu
versionsobe a cada transição; ignore estado comversion≤ a já aplicada (webhooks fora de ordem, webhooks.md).approved→processing: no código, o saque viraprocessingquando chega o primeiro retorno do PSP (enviado/aceito/liquidado), não quando o comando é publicado. Se o envio é recusado antes de sair (reserva de caixa recusada, guarda de ambiente), o saque fica emapprovede o Dracma alerta.requested → cancelledexiste no código (o texto do §6.2 permite; o diagrama do contrato não desenha).paid → returnedhoje só acontece pela ação de sandboxreturn(o executor ainda não publica devolução real — D-W6 em divergencias.md). Trate assim mesmo.- Os acréscimos da v1.1.0 (
pending_compliance,sent,in_transit,recall_requested,recalled) estão em Estados futuros.
O que fazer com a reserva em cada estado¶
| Estado (lido no fetch-back) | Efeito no seu livro | Observação |
|---|---|---|
requested |
mantém a reserva | decisão em curso (ex.: consulta DICT assíncrona, até ~5 min) |
under_review |
mantém a reserva | humano decide; motivos em decision.reasons |
approved |
mantém a reserva | aprovado; aguarda envio (horário do automático, caixa) |
processing |
mantém a reserva | enviado ao PSP |
paid |
finaliza: a reserva vira saída definitiva | psp_reference = E2E id do PIX |
rejected |
devolve a reserva ao jogador | motivo em decision.reasons |
failed |
devolve a reserva ao jogador | motivo em failure_code; o dinheiro não saiu |
cancelled |
devolve a reserva ao jogador | por você, pelo painel ou BLOCKLIST_HIT_AT_DISPATCH |
returned (depois de paid) |
política do operador | o dinheiro volta ao caixa da operação no Dracma; recomendação: não mover dinheiro automaticamente — needs_decision + alerta, humano decide recreditar |
| estado desconhecido | mantém a reserva | não-terminal; confira depois (Contrato §6.0) |
Devolver e finalizar são idempotentes pelo external_id: dois eventos do mesmo desfecho movem uma vez.
Criar saque — POST /withdrawals¶
Escopo exigido: withdrawals:write.
Pedido¶
POST /v1/withdrawals HTTP/1.1
Host: sandbox.dracma.example
Content-Type: application/json; charset=utf-8
Idempotency-Key: wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c
Dracma-Key-Id: key_test_01J9EXEMPLO
Dracma-Timestamp: 1790000000
Dracma-Nonce: <32 hex CSPRNG>
Dracma-Signature: v1=<assinatura>
{
"external_id": "wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c",
"player": {
"external_ref": "0192a3b4-0000-7000-8000-000000000001",
"document": { "type": "CPF", "number": "12345678909", "country": "BR" },
"name": "Jogador Teste",
"ip": "203.0.113.10",
"device_id": "dev_7f3a"
},
"amount": { "amount": "5000", "currency": "BRL" },
"method": "pix",
"destination": {
"pix": { "key_type": "cpf", "key": "12345678909" }
},
"review_requested": false
}
| Campo | Obrigatório | Regra (código atual) |
|---|---|---|
external_id |
sim | único por operação, ^[A-Za-z0-9_.:-]{1,64}$ |
player |
sim | mesmo formato do depósito; documento fixado no primeiro uso; name ≤ 120, device_id ≤ 128 |
player.ip |
na prática, sim | IP do jogador (não do seu servidor). Sem ele o saque nunca é automático: vai a under_review com OWNERSHIP_UNVERIFIED (o PSP de saque exige o IP do titular). IP inválido → 400 (format) |
amount |
sim | Money, amount > 0 ("0" → 400) |
method |
sim | pix |
destination |
sim | exatamente um destino (reason: one_of), com a mesma chave do method (reason: method_mismatch): {"pix": {"key_type", "key"}} com key_type ∈ cpf, cnpj, email, phone, random (enum) e key de 1–140 caracteres (format) |
review_requested |
não | true → sempre revisão manual (OPERATOR_REVIEW_REQUESTED). Só torna a decisão mais restritiva |
review_reasons |
não | seus motivos; aparecem no painel prefixados OP: |
player_context |
não | total_deposited, total_withdrawn, account_created_at, net_gaming_result — informativo, nunca libera automático |
metadata |
não | até 20 chaves string |
Titularidade do destino: chave cpf precisa ser o CPF do jogador; cnpj para pessoa física é bloqueio;
email/phone/random são conferidas no DICT. Com o DICT fora, a resposta é 201 com status: "requested";
o Dracma reconsulta com backoff por até ~5 min e depois decide (approved, rejected se o titular diverge, ou
under_review com DICT_UNAVAILABLE).
A validação estrita do corpo é a mesma do depósito (campo desconhecido, chave duplicada, UTF-8 inválido,
tipo errado → 400 validation_failed com errors[].reason; ver depositos.md).
Resposta 201 — saque registrado com decisão¶
O código responde sempre 201 na criação (nunca 202): a criação não espera o PSP (D-W9 em
divergencias.md).
Automático:
{
"id": "wdr_0192a3b40c6d7e8f9a0b1c2d3e4f5a6c",
"external_id": "wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c",
"status": "approved",
"version": 1,
"amount": { "amount": "5000", "currency": "BRL" },
"fee": null,
"method": "pix",
"player_ref": "0192a3b4-0000-7000-8000-000000000001",
"decision": {
"mode": "automatic",
"reasons": [],
"ruleset_version": 7,
"decided_by": [],
"decided_at": "2026-09-26T15:00:01Z"
},
"psp_reference": null,
"failure_code": null,
"created_at": "2026-09-26T15:00:00Z",
"updated_at": "2026-09-26T15:00:01Z",
"paid_at": null,
"rail": null, "fees": [], "charge_bearer": null, "fx": null, "purpose": null, "compliance": null,
"expected_settlement_date": null, "sent_at": null, "tracking": null, "return": null, "recall": null
}
Revisão manual (trecho):
{
"id": "wdr_0192a3b40c6d7e8f9a0b1c2d3e4f5a6d",
"status": "under_review",
"decision": {
"mode": "manual",
"reasons": ["FIRST_WITHDRAWAL", "PIX_KEY_CHANGED"],
"ruleset_version": 7,
"decided_by": [],
"decided_at": null
}
}
statusna resposta:approved(automático; viraprocessingquando o PSP responder),under_review(manual) ourequested(DICT pendente:decision.mode = "manual",reasons: [],decided_at: null).- O saque nasce com
version1 já no estado decidido (não há evento públicowithdrawal.requested).decision.decided_atsó vem preenchido quando a decisão foi tomada (automático, ou depois da revisão). - Os campos v1.1.0 (
rail,fx,tracking…) vêm semprenull/vazios em PIX. approvedfora do horário do automático espera a abertura da janela; antes do envio o Dracma reconfere listas e caixa — se uma lista pegar, o saque vai acancelledcomBLOCKLIST_HIT_AT_DISPATCH; se faltar caixa, fica emapprovede o Dracma alerta.- O resultado final (
paid,failed,rejected, …) chega por webhookwithdrawal.*; aja só depois do fetch-back (webhooks.md).paid_até o instante em que o Dracma registrou a liquidação. - Motor de regras fora na criação não é
503: o saque é criado emunder_reviewcomRULES_UNAVAILABLE(na dúvida, revisão humana). Motor com resposta incoerente →RULES_INVALID_RESPONSE.
Recusas síncronas (422, sem objeto)¶
Bloqueio duro responde 422 sem criar o saque. É decisão: devolva a reserva ao jogador e não repita.
A mesma Idempotency-Key repete o mesmo 422; nova tentativa exige external_id novo (e nova reserva).
Códigos 422 que o código aceita do motor (bloqueio com código fora desta lista vira revisão, não 422):
code |
decision-código (REGRAS §12) |
Quando |
|---|---|---|
amount_below_minimum / amount_above_maximum |
AMOUNT_BELOW_MINIMUM / AMOUNT_ABOVE_MAXIMUM |
fora do mínimo/máximo por transação |
document_invalid |
DOCUMENT_INVALID |
CPF/CNPJ com dígito errado |
document_mismatch |
PLAYER_DOCUMENT_CHANGED, DOCUMENT_MISMATCH |
documento ≠ fixado; chave PIX de outro titular |
player_blocked |
PLAYER_BLOCKED |
jogador em lista de bloqueio |
destination_blocked |
DESTINATION_BLOCKED |
chave PIX em lista de bloqueio |
velocity_exceeded |
VELOCITY_HARD_LIMIT, WINDOW_HARD_LIMIT |
limite duro de janela/velocidade |
insufficient_operation_balance |
INSUFFICIENT_OPERATION_BALANCE |
o caixa da operação no Dracma não cobre o saque; pode repetir mais tarde com nova Idempotency-Key e novo external_id |
Outros erros e a regra de repetição: erros-e-idempotencia.md. Em especial:
400 currency_not_enabled/method_not_enabled→ configuração da operação; devolva a reserva;- 429
rate_limitedtambém vem do limite de 3 saques pedidos por jogador por hora (SEGURANCA §9.1, divergencias.md D-A9): não cria o saque; mantenha a reserva e repita depois doRetry-After; - erro de rede, timeout, 5xx, 429 → repita com a mesma
Idempotency-Keyaté resposta conclusiva; não devolva a reserva; na dúvida,GET /withdrawals?external_id=<seu id>; - 409
duplicate_external_id→ o saque já existe; o objeto vem emexisting. Siga pelo estado dele, não crie outro; - 409
idempotency_key_reused→ mesma chave com corpo diferente: bug do seu lado; mantenha a reserva e investigue (recomendação:needs_decision).
decision.reasons — por que um saque foi para revisão ou recusado¶
Códigos estáveis (novos só se acrescentam; código desconhecido = trate como "revisão", sem efeito de
dinheiro). Tipo R = revisão (under_review); B = bloqueio (rejected ou 422).
| Código | Tipo | Significado para o operador |
|---|---|---|
AUTO_MULTIPLIER_EXCEEDED |
R | o saque (somado aos anteriores, inclusive pendentes) passa de X vezes o que o jogador depositou e o Dracma liquidou |
NO_SETTLED_DEPOSIT |
R | o jogador não tem depósito liquidado pelo Dracma nesta moeda |
AUTO_DISABLED |
R | automático desligado na moeda (multiplicador "0") |
OPERATION_AUTO_SUSPENDED |
R | automático suspenso na operação inteira (interruptor de emergência) |
AUTO_MAX_PER_WITHDRAWAL_EXCEEDED |
R | acima do teto do automático por saque |
FIRST_WITHDRAWAL |
R | primeiro saque pago do jogador nesta operação — sempre humano (default) |
OPERATOR_REVIEW_REQUESTED |
R | você enviou review_requested: true |
OWNERSHIP_UNVERIFIED |
R | titularidade não confirmada — inclui saque sem player.ip |
DICT_UNAVAILABLE |
R | consulta DICT indisponível por mais de ~5 min |
RULES_UNAVAILABLE / RULES_INVALID_RESPONSE |
R | motor de regras fora / resposta incoerente: na dúvida, revisão (fora do catálogo REGRAS §12, D-D7) |
WINDOW_DAY_AMOUNT_EXCEEDED, WINDOW_DAY_COUNT_EXCEEDED, WINDOW_7D_AMOUNT_EXCEEDED, WINDOW_7D_COUNT_EXCEEDED, WINDOW_30D_AMOUNT_EXCEEDED, WINDOW_30D_COUNT_EXCEEDED |
R | soma ou quantidade de saques do jogador na janela passou do limiar de revisão |
VELOCITY_WITHDRAWALS |
R | muitos pedidos em pouco tempo |
PIX_KEY_CHANGED |
R | chave PIX nova para o jogador ou ainda em carência |
DESTINATION_CHANGED |
R | idem para destinos não-PIX (futuro) |
NEW_CONTEXT |
R | acompanha PIX_KEY_CHANGED: IP ou device nunca vistos para o jogador |
DESTINATION_CHURN |
R | destinos distintos demais em 30 dias |
SHARED_DESTINATION |
R | a mesma chave PIX aparece em saques de jogadores diferentes |
SHARED_DOCUMENT |
R | o mesmo documento está em mais de um external_ref (multi-conta) |
IP_LISTED / DEVICE_LISTED |
R | IP ou device do jogador em lista de revisão |
VIP_LIMIT_EXCEEDED |
R | jogador VIP estourou o limite próprio |
OPERATION_AUTO_CAP_EXCEEDED |
R | teto agregado de saques automáticos da operação (hora/dia) atingido |
OUTSIDE_AUTO_HOURS |
R | fora do horário do automático, com política review |
DUAL_APPROVAL_REQUIRED |
R | valor acima do limiar de 4-eyes ou motivo que exige duas pessoas |
AML_STRUCTURING |
R | fracionamento logo abaixo de um limiar de reporte |
AML_RAPID_TURNAROUND |
R | saque logo após depósito, em valor próximo, de forma repetida |
AML_NO_PLAY_REPORTED |
R | você informou net_gaming_result = "0" com depósitos |
AML_SHARED_PAYER / AML_MANY_PAYERS |
R | pagadores compartilhados entre jogadores / muitos pagadores para um jogador |
CRYPTO_ADDRESS_RISK |
R | risco de endereço cripto (não se aplica a PIX) |
DOCUMENT_MISMATCH |
B | chave PIX de outro titular descoberta depois do 201 → rejected |
PLAYER_BLOCKED / DESTINATION_BLOCKED |
B | lista atualizada antes do envio → rejected |
BLOCKLIST_HIT_AT_DISPATCH |
B | lista pegou no momento do envio de um approved → cancelled |
OP:<texto> |
— | só exibição: os seus review_reasons, prefixados |
Os limiares (X×, janelas, carência de chave nova, horários) são do conjunto de regras da operação; leia
com GET /v1/rules. Revisão com qualquer código R = 1 revisor; DUAL_APPROVAL_REQUIRED = 2 revisores.
Aprovado vai a approved; recusado a rejected (devolva a reserva).
Consultar saque — GET /withdrawals/{id}¶
Escopo read. Estado autoritativo usado no fetch-back. 200 → Saque; 404 not_found (inclusive objeto de
outra operação, ou id fora do formato wdr_[A-Za-z0-9]+).
Listar saques — GET /withdrawals¶
Escopo read. Balde de listagem: 5 req/s por chave. Filtros external_id, player_ref, status (enum
StatusSaque, senão 400 com reason: enum), updated_since (RFC 3339), cursor (opaco; hoje base64url),
limit (1–200, default 50). Resposta {"data": [Saque…], "next_cursor": … | null}.
GET /v1/withdrawals?external_id=wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c HTTP/1.1
É assim que você descobre o destino de um POST cuja resposta se perdeu.
Cancelar saque — POST /withdrawals/{id}/cancel¶
Escopo withdrawals:write. Corpo vazio, Idempotency-Key obrigatória.
- Permitido em
requested,under_reviewe emapprovedenquanto o comando de envio ao PSP não saiu. 200 com o objeto emcancelled(ou o mesmo 200 se já estava cancelado). Devolva a reserva quando lercancelled. approvedcom o envio já publicado,processingou depois →409 invalid_state_transition: o dinheiro pode já estar saindo; espere o desfecho (paid/failed). Isto é mais restritivo que o contrato (D-W2 em divergencias.md).
POST /v1/withdrawals/wdr_0192a3b40c6d7e8f9a0b1c2d3e4f5a6c/cancel HTTP/1.1
Idempotency-Key: cnl_wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c
Dracma-Key-Id: key_test_01J9EXEMPLO
Dracma-Timestamp: 1790000200
Dracma-Nonce: <32 hex CSPRNG>
Dracma-Signature: v1=<assinatura>
Limites do jogador — GET /players/{external_ref}/limits¶
Escopo read. currency obrigatório na query (moeda não habilitada → 400). Jogador que nunca transacionou
nesta operação → 404. Mostra a posição do jogador perante as regras — útil para avisar "até R$ X sai na
hora".
GET /v1/players/0192a3b4-0000-7000-8000-000000000001/limits?currency=BRL HTTP/1.1
{
"player_ref": "0192a3b4-0000-7000-8000-000000000001",
"currency": "BRL",
"total_deposited": { "amount": "10000", "currency": "BRL" },
"total_withdrawn": { "amount": "5000", "currency": "BRL" },
"auto_multiplier": "1",
"auto_withdrawal_remaining": { "amount": "5000", "currency": "BRL" },
"blocked": false
}
auto_withdrawal_remaining é quanto ainda cabe na regra de X×; outras regras (primeiro saque, chave nova,
janelas, IP ausente) ainda podem mandar para revisão. Motor de regras fora → 503 retryable.
Caixa da operação — GET /balances¶
Escopo read. Saldo de liquidação da operação por moeda — o dinheiro que paga os saques.
{
"data": [
{
"available": { "amount": "150000000", "currency": "BRL" },
"pending_in": { "amount": "250000", "currency": "BRL" },
"pending_out": { "amount": "90000", "currency": "BRL" },
"as_of": "2026-09-26T15:00:00Z"
}
]
}
availableepending_outvêm do livro do Dracma;pending_iné o dinheiro recebido e ainda não creditado (depósitos emunder_review);as_ofé o instante da leitura. Moeda habilitada sem movimento vem zerada. Livro fora →503retryable.- Saque sem caixa →
422 insufficient_operation_balancena criação. Se faltar caixa só no envio, o saque fica emapprovede o Dracma alerta. Monitore o caixa e alerte antes de zerar (go-live-operador.md).
Fluxo recomendado¶
- Transação local: grave o saque (
external_id, valor, destino) e reserve o saldo. POST /withdrawalscomIdempotency-Key = external_id,player.ipdo jogador, timeout 10 s.- 201 → guarde
id,status,decision. 4xx → devolva a reserva. Rede/5xx/429 → repita com a mesma chave. - Webhook
withdrawal.*→ verifique, deduplique, fetch-backGET /withdrawals/{id}e aplique a tabela de estados acima. - Job de
GET /events?after=a cada 5 min e conciliação diária (reconciliacao.md).
Testar aprovação, recusa, pagamento, falha e devolução no sandbox: sandbox.md
(/sandbox/withdrawals/{id}/{approve|reject|pay|fail|return}).
Estados futuros (leitor tolerante)¶
A v1.1.0 acrescentou estados para métodos não disponíveis ainda (wire/swift) e para screening de
sanções: pending_compliance, sent, in_transit, recall_requested, recalled, com os eventos
withdrawal.pending_compliance, withdrawal.sent, withdrawal.in_transit, withdrawal.recall_requested,
withdrawal.recall_rejected e withdrawal.recalled. A rota POST /withdrawals/{id}/recall só vale para
wire/SWIFT: num saque PIX ela responde 409 invalid_state_transition sem mudar nada (corpo inválido → 400;
saque inexistente ou de outra operação → 404).
Regra (Contrato §6.0) que o seu código DEVE seguir desde já:
- estado ou evento desconhecido = não-terminal: mantenha a reserva, não debite nem devolva,
responda 200 ao webhook e confira de novo depois (fetch-back e
GET /events?after=); pending_compliancePODE aparecer em PIX quando uma lista de sanções casa — pela regra acima, nada se move;withdrawal.recall_rejectedvolta o saque a um estado anterior comversionmaior: para quem só conhece PIX, é umpaidjá aplicado — idempotente;- campo JSON desconhecido é ignorado; valor desconhecido de
decision.reasonsnão muda o efeito.