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/cancelled lido 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_ref opaco e imutável, nunca e-mail/telefone.
  • Todo POST leva Idempotency-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
  • version sobe a cada transição; ignore estado com version ≤ a já aplicada (webhooks fora de ordem, webhooks.md).
  • approved → processing: no código, o saque vira processing quando 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 em approved e o Dracma alerta.
  • requested → cancelled existe no código (o texto do §6.2 permite; o diagrama do contrato não desenha).
  • paid → returned hoje só acontece pela ação de sandbox return (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
  }
}
  • status na resposta: approved (automático; vira processing quando o PSP responder), under_review (manual) ou requested (DICT pendente: decision.mode = "manual", reasons: [], decided_at: null).
  • O saque nasce com version 1 já no estado decidido (não há evento público withdrawal.requested). decision.decided_at só 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 sempre null/vazios em PIX.
  • approved fora 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 a cancelled com BLOCKLIST_HIT_AT_DISPATCH; se faltar caixa, fica em approved e o Dracma alerta.
  • O resultado final (paid, failed, rejected, …) chega por webhook withdrawal.*; 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 em under_review com RULES_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_limited també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 do Retry-After;
  • erro de rede, timeout, 5xx, 429 → repita com a mesma Idempotency-Key até 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 em existing. 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_review e em approved enquanto o comando de envio ao PSP não saiu. 200 com o objeto em cancelled (ou o mesmo 200 se já estava cancelado). Devolva a reserva quando ler cancelled.
  • approved com o envio já publicado, processing ou 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"
    }
  ]
}
  • available e pending_out vêm do livro do Dracma; pending_in é o dinheiro recebido e ainda não creditado (depósitos em under_review); as_of é o instante da leitura. Moeda habilitada sem movimento vem zerada. Livro fora → 503 retryable.
  • Saque sem caixa → 422 insufficient_operation_balance na criação. Se faltar caixa só no envio, o saque fica em approved e o Dracma alerta. Monitore o caixa e alerte antes de zerar (go-live-operador.md).

Fluxo recomendado

  1. Transação local: grave o saque (external_id, valor, destino) e reserve o saldo.
  2. POST /withdrawals com Idempotency-Key = external_id, player.ip do jogador, timeout 10 s.
  3. 201 → guarde id, status, decision. 4xx → devolva a reserva. Rede/5xx/429 → repita com a mesma chave.
  4. Webhook withdrawal.* → verifique, deduplique, fetch-back GET /withdrawals/{id} e aplique a tabela de estados acima.
  5. 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_compliance PODE aparecer em PIX quando uma lista de sanções casa — pela regra acima, nada se move;
  • withdrawal.recall_rejected volta o saque a um estado anterior com version maior: para quem só conhece PIX, é um paid já aplicado — idempotente;
  • campo JSON desconhecido é ignorado; valor desconhecido de decision.reasons não muda o efeito.