Saques

Para: Dev — Conferido em 2026-10-18.

Um saque é um pedido de pagamento PIX 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. Esta página descreve as quatro rotas (criar, consultar, listar e cancelar), o destino PIX, as recusas na ordem em que a API as confere e os campos do objeto Saque, inclusive o objeto return de um saque devolvido pelo banco.

O que é

Rota Escopo operationId Resposta de sucesso
POST /v1/withdrawals withdrawals:write criarSaque sempre 201 com o Saque e a decisão
GET /v1/withdrawals read listarSaques 200 com {data, next_cursor}
GET /v1/withdrawals/{id} read obterSaque 200 com o Saque
POST /v1/withdrawals/{id}/cancel withdrawals:write cancelarSaque 200 com o Saque em cancelled

O quinto POST, POST /v1/withdrawals/{id}/recall, é uma rota reservada e está em Rotas e campos reservados.

Hoje só existe PIX em BRL.

method Situação
pix disponível
card, crypto, boleto, bank_transfer, wire, swift não disponível: 400 method_not_enabled
qualquer outro valor 400 validation_failed com field: method e reason: enum

Não disponível

O Dracma não paga saque por cartão, cripto, boleto, transferência bancária, wire nem SWIFT. A tentativa responde 400 method_not_enabled e não cria nada. O único destino aceito é uma chave PIX.

O PSP que paga o saque é escolhido pelo Dracma para a sua operação. Isso não muda nada na API. Veja PSPs.

Quando usar

Regra nº 1: reserve antes de pedir. Debite (reserve) o saldo do jogador antes de chamar POST /v1/withdrawals, na mesma transação em que você grava o saque no seu lado (external_id, valor e destino). Assim:

  • 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: um 4xx síncrono, ou o estado rejected, failed ou cancelled lido pela API.

Nunca devolva a reserva sem resposta conclusiva. Timeout, erro de rede, 5xx e 429 não são conclusivos: repita com a mesma chave ou consulte GET /v1/withdrawals?external_id=<seu id>.

sequenceDiagram participant C as Seu conector participant D as API do Dracma participant W as Webhook do Dracma C->>C: grava o saque e reserva o saldo (mesma transação) C->>D: POST /v1/withdrawals (Idempotency-Key = external_id) D-->>C: 201 approved, under_review ou requested, com decision Note over C,D: 4xx síncrono: devolva a reserva. Rede, 5xx, 429: repita com a mesma chave W->>C: POST withdrawal.paid, rejected, failed... (assinado) C->>C: verifica assinatura e deduplica C->>D: GET /v1/withdrawals/{id} (fetch-back) D-->>C: 200 com o status autoritativo C->>C: finaliza ou devolve a reserva, conforme o estado
  1. Transação local: grave o saque e reserve o saldo.
  2. POST /v1/withdrawals com Idempotency-Key igual ao external_id, player.ip do jogador e timeout de 10 s.
  3. 201: guarde id, status e decision. 4xx: devolva a reserva. Rede, 5xx ou 429: repita com a mesma chave.
  4. Webhook withdrawal.*: verifique, deduplique, faça o fetch-back em GET /v1/withdrawals/{id} e aplique a tabela de estados.
  5. Um job com GET /v1/events?after= a cada 5 minutos e a reconciliação diária fecham as lacunas.

A tabela de efeito de cada estado na reserva está em Ciclo do saque.

Como funciona

Criar: POST /v1/withdrawals

O corpo é um objeto JSON. A validação é estrita (a mesma do depósito): campo desconhecido, chave repetida, UTF-8 inválido, tipo errado e conteúdo depois do objeto voltam 400 validation_failed com errors[].reason. Veja Depósitos para a lista.

Campo Obrigatório Regra
external_id sim único por operação, ^[A-Za-z0-9_.:-]{1,64}$
player sim mesmo formato do depósito. O documento fica fixado no primeiro uso. name até 120 caracteres, device_id até 128
player.ip na prática, sim IP do jogador, não do seu servidor. Sem ele o saque nunca é automático: vai para under_review com OWNERSHIP_UNVERIFIED. IP inválido: 400 com reason: format
amount sim Money com amount maior que zero
method sim pix
destination sim exatamente um destino, com a mesma chave de method: {"pix": {"key_type", "key"}}
review_requested não true obriga revisão manual (OPERATOR_REVIEW_REQUESTED). Só torna a decisão mais restritiva
review_reasons não seus motivos; aparecem no painel com o prefixo OP:
player_context não total_deposited, total_withdrawn, account_created_at, net_gaming_result. Informativo: nunca libera o automático
metadata não até 20 chaves, valores texto

player.email, player.birth_date e player.kyc_level são aceitos pela forma do Jogador, mas o saque não os usa. purpose, fx_quote_id e payout_currency também são aceitos pela forma e ignorados: o saque PIX não tem câmbio (veja Divergências conhecidas).

Destino PIX

destination.pix tem key_type e key. O key_type é um dos cinco abaixo; o Dracma normaliza a chave antes de comparar e de guardar.

key_type Chave Normalização feita pelo Dracma
cpf CPF do titular só dígitos
cnpj CNPJ só dígitos
email e-mail minúsculas
phone telefone + seguido dos dígitos
random chave aleatória (EVP) minúsculas, sem espaços
  • key tem de 1 a 140 caracteres. Fora disso: 400 com reason: format. key_type fora da tabela: 400 com reason: enum.
  • Se você tem um cadastro com nomes antigos de tipo (evp, aleatoria, telefone), converta para random e phone antes de enviar.
  • destination com mais de uma chave: reason: one_of. Chave diferente de method: reason: method_mismatch.
  • A titularidade é conferida na decisão: chave cpf precisa ser o CPF do jogador; cnpj para pessoa física é bloqueio; email, phone e random são conferidas na consulta ao diretório do PIX. Com a consulta indisponível, a resposta é 201 com status: "requested"; o Dracma reconsulta por alguns minutos e depois decide (approved, rejected se o titular diverge, ou under_review).

Resposta 201 e a decisão

O código responde sempre 201 na criação, nunca 202: a criação não espera o PSP. O status da resposta é um destes:

status Quando decision.mode decision.decided_at
approved o motor decidiu o automático automatic preenchido
under_review decisão manual (regra, review_requested, sem player.ip, motor fora ou resposta incoerente) manual null
requested consulta ao diretório do PIX pendente manual, com reasons: [] null
  • O saque nasce com version 1 já no estado decidido. Não há evento público withdrawal.requested.
  • 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.
  • 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). Resposta incoerente do motor resulta em RULES_INVALID_RESPONSE.
  • O significado de cada código de decision.reasons está em Decisão, limites e regras. Os limiares são da regra da operação: leia com Regras (API).

Recusas síncronas 422 (sem objeto)

Bloqueio duro responde 422 sem criar o saque. É uma decisão: devolva a reserva e não repita. A mesma Idempotency-Key repete o mesmo 422; uma nova tentativa exige external_id novo (e nova reserva). Bloqueio com código fora desta lista vira revisão, não 422.

code Quando
amount_below_minimum, amount_above_maximum fora do mínimo ou máximo por transação
document_invalid CPF ou CNPJ com dígito verificador errado
document_mismatch documento diferente do fixado, ou chave PIX de outro titular
player_blocked jogador em lista de bloqueio
destination_blocked chave PIX em lista de bloqueio
velocity_exceeded limite duro de janela ou velocidade
insufficient_operation_balance o caixa da operação no Dracma não cobre o saque. Pode repetir mais tarde, com Idempotency-Key e external_id novos

Consultar: GET /v1/withdrawals/{id}

Escopo read. É o estado autoritativo do fetch-back. 200 devolve o Saque; 404 not_found quando o id não existe nesta operação (inclusive objeto de outra operação) ou está fora do formato wdr_ seguido de letras e números.

Listar: GET /v1/withdrawals

Escopo read, com o balde de listagem do rate limit (veja Limites e quotas).

Parâmetro Uso
external_id acha o saque pelo seu id. É como você descobre o destino de um POST cuja resposta se perdeu
player_ref saques de um jogador
status enum StatusSaque. Valor fora do enum: 400 com reason: enum
updated_since RFC 3339 (aceita fração de segundo). Devolve updated_at maior ou igual ao instante. Formato inválido: reason: format
cursor o next_cursor da página anterior. Inválido: reason: format
limit de 1 a 200. Padrão 50. Fora da faixa: reason: range

Resposta: {"data": [Saque, ...], "next_cursor": "..." | null}. A ordem é por created_at e depois id, crescente. O next_cursor é opaco (hoje um texto em base64url): não monte nem interprete. Veja Paginação e filtros.

Cancelar: POST /v1/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 ainda não saiu. A resposta é 200 com o objeto em cancelled, ou o mesmo 200 se ele já estava cancelled. Devolva a reserva quando ler cancelled.
  • approved com o envio já publicado, processing ou qualquer estado depois: 409 invalid_state_transition. O dinheiro pode já estar saindo; espere o desfecho (paid ou failed). Isso é mais restritivo que o contrato (D-W2 em Divergências conhecidas).
  • id inexistente nesta operação: 404 not_found.

Objeto Saque

O Saque é o mesmo no 201, no GET e no data.object do webhook. Os campos da v1.1.0 vêm sempre null ou vazios em PIX, com uma exceção: return.

Campo Tipo Conteúdo
id texto wdr_ seguido de 32 caracteres hexadecimais. Trate como opaco
external_id texto o seu id
status texto estado atual
version inteiro começa em 1 e sobe a cada transição. Ignore estado com version menor ou igual à já aplicada
amount Money valor pedido
fee null sempre null: o Dracma não desconta tarifa do saque
method texto pix
player_ref texto o seu player.external_ref
decision objeto mode (automatic ou manual), reasons, ruleset_version, decided_by (ids opacos dos revisores) e decided_at
psp_reference texto ou null identificador do pagamento PIX (E2E), preenchido quando o saque é pago
failure_code texto ou null motivo técnico em failed. Lista aberta
created_at, updated_at, paid_at texto RFC 3339 em UTC. Podem trazer fração de segundo (diferente do depósito)
return objeto ou null preenchido só em returned. Veja abaixo
rail, fees, charge_bearer, fx, purpose, compliance, expected_settlement_date, sent_at, tracking, recall null, [] reservados da v1.1.0

Faça o parse de datas aceitando ou não fração de segundo (leitor tolerante).

Reservado no contrato

rail, fees, charge_bearer, fx, purpose, compliance, expected_settlement_date, sent_at, tracking e recall estão no schema Saque, mas a API não os preenche em saque PIX. fees vem [] e os demais null. fee vem sempre null: as tarifas estão no contrato com o Dracma (veja Tarifas).

status e transições

stateDiagram-v2 [*] --> requested [*] --> approved [*] --> under_review requested --> approved requested --> under_review requested --> rejected requested --> cancelled under_review --> approved under_review --> rejected under_review --> cancelled approved --> processing approved --> cancelled processing --> paid processing --> failed paid --> returned
status Significado Efeito na sua reserva
requested decisão em curso (consulta ao diretório do PIX) mantém
under_review revisão humana; motivos em decision.reasons mantém
approved aprovado; aguarda envio (horário do automático, caixa) mantém
processing enviado ao PSP mantém
paid pago finaliza: a reserva vira saída definitiva
rejected, failed, cancelled o dinheiro não saiu devolve a reserva
returned o banco devolveu depois de paid decisão sua. Veja Ciclo do saque
pending_compliance, sent, in_transit, recall_requested, recalled acréscimos da v1.1.0 não alcançáveis em PIX; só existem como filtro de status
desconhecido — mantém a reserva e confira de novo (leitor tolerante)

Devolver e finalizar são idempotentes pelo external_id: dois eventos do mesmo desfecho movem uma vez.

O objeto return

Quando o banco devolve um saque já pago, o saque vai de paid para returned e o campo return passa a vir preenchido:

Campo Tipo Conteúdo
reason_code texto código curto do motivo informado pelo PSP (letras minúsculas, números e _, até 64 caracteres). unspecified quando o PSP não informou
returned_amount Money o valor devolvido. Pode ser menor que amount (devolução parcial) e nunca é maior
returned_at texto instante da devolução, RFC 3339 em UTC
  • Devolução parcial continua returned: não existe um estado "parcialmente devolvido". Confira returned_amount contra amount.
  • O Dracma credita de volta ao caixa da operação só o valor devolvido e registra um item return na reconciliação (veja Reconciliação).
  • reason e return_fee do schema DevolucaoBancaria não são devolvidos em saque PIX.
  • Fora de returned, return é null.

O que fazer com o jogador quando vem returned é política sua. A recomendação é não mover dinheiro automaticamente: marque como exceção, alerte e decida com um humano (veja Política de exceções de dinheiro).

Exemplos

Pedido de criação (os valores de assinatura são ilustrativos):

POST /v1/withdrawals HTTP/1.1
Host: api.example.test
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: 0123456789abcdef0123456789abcdef
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
}

Resposta 201, decisão automática:

{
  "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-10-18T15:00:01.250Z"
  },
  "psp_reference": null,
  "failure_code": null,
  "created_at": "2026-10-18T15:00:00.812Z",
  "updated_at": "2026-10-18T15:00:01.250Z",
  "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
}

Resposta 201, revisão manual (primeiro saque e chave nova):

{
  "id": "wdr_0192a3b40c6d7e8f9a0b1c2d3e4f5a6d",
  "external_id": "wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6d",
  "status": "under_review",
  "version": 1,
  "amount": { "amount": "5000", "currency": "BRL" },
  "fee": null,
  "method": "pix",
  "player_ref": "0192a3b4-0000-7000-8000-000000000002",
  "decision": {
    "mode": "manual",
    "reasons": ["FIRST_WITHDRAWAL", "PIX_KEY_CHANGED"],
    "ruleset_version": 7,
    "decided_by": [],
    "decided_at": null
  },
  "psp_reference": null,
  "failure_code": null,
  "created_at": "2026-10-18T15:02:00.100Z",
  "updated_at": "2026-10-18T15:02:00.100Z",
  "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
}

Um saque devolvido pelo banco, com devolução parcial (R$ 30,00 de R$ 50,00):

{
  "id": "wdr_0192a3b40c6d7e8f9a0b1c2d3e4f5a6c",
  "external_id": "wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c",
  "status": "returned",
  "version": 5,
  "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-10-18T15:00:01.250Z"
  },
  "psp_reference": "E00000000202610181500abcdef0123456",
  "failure_code": null,
  "created_at": "2026-10-18T15:00:00.812Z",
  "updated_at": "2026-10-19T09:30:12.004Z",
  "paid_at": "2026-10-18T15:00:09.500Z",
  "rail": null,
  "fees": [],
  "charge_bearer": null,
  "fx": null,
  "purpose": null,
  "compliance": null,
  "expected_settlement_date": null,
  "sent_at": null,
  "tracking": null,
  "return": {
    "reason_code": "unspecified",
    "returned_amount": { "amount": "3000", "currency": "BRL" },
    "returned_at": "2026-10-19T09:30:11Z"
  },
  "recall": null
}

Recusa 422, caixa insuficiente (nenhum saque foi criado):

{
  "type": "https://docs.dracma.test/errors/insufficient_operation_balance",
  "title": "insufficient_operation_balance",
  "status": 422,
  "code": "insufficient_operation_balance",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "retryable": false
}

Para descobrir o destino de um POST cuja resposta se perdeu:

GET /v1/withdrawals?external_id=wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c HTTP/1.1

Dinheiro real

Em uma operação Real, POST /v1/withdrawals paga PIX de verdade. Faça os testes de aprovação, recusa, pagamento, falha e devolução numa operação Teste, com as ações de Sandbox.

Erros e casos de borda

Erros da criação, na ordem em que a API os confere

# HTTP code Quando O que fazer
1 400 validation_failed corpo inválido: forma, method ausente ou fora do enum (enum), destino (one_of, method_mismatch, enum, format), player.ip inválido (format) corrija; devolva a reserva
2 429 rate_limited mais de 3 saques pedidos por jogador por hora mantenha a reserva e repita depois do Retry-After, com a mesma chave
3 400 currency_not_enabled moeda não habilitada na operação configuração do onboarding; devolva a reserva
4 400 method_not_enabled method diferente de pix, ou método não habilitado devolva a reserva
5 409 duplicate_external_id o external_id já existe; o objeto vem em existing siga pelo estado do existing; não crie outro
6 422 os da tabela de recusas bloqueio duro da decisão devolva a reserva; não repita igual
7 503 service_unavailable cofre de dados ou banco fora repita com a mesma chave; mantenha a reserva
  • Rede, timeout, 5xx e 429: repita com a mesma Idempotency-Key até uma resposta conclusiva e não devolva a reserva. Na dúvida, consulte GET /v1/withdrawals?external_id=.
  • 409 idempotency_key_reused: mesma chave com corpo diferente. É um bug do seu lado: mantenha a reserva e investigue.
  • Regras de repetição e retenção da chave: Idempotência e repetição.

Outras rotas

Rota Situação Resposta
GET /v1/withdrawals/{id} não existe nesta operação, ou id fora do formato 404 not_found
GET /v1/withdrawals status, updated_since, limit ou cursor inválidos 400 validation_failed
POST /v1/withdrawals/{id}/cancel envio já publicado, processing ou depois 409 invalid_state_transition
qualquer sem o escopo exigido 403 insufficient_scope

Casos de borda

  • approved pode durar. Se o envio é recusado antes de sair (caixa insuficiente ou guarda de ambiente), o saque fica em approved e o Dracma alerta. Mantenha a reserva.
  • requested não é revisão. Espere: o Dracma decide sozinho depois de alguns minutos.
  • Sem player.ip o saque vai sempre para revisão. Mande o IP do jogador em todo pedido.
  • Estado, motivo ou campo desconhecido nunca devem derrubar o seu parser: trate como não-terminal, mantenha a reserva e confira de novo pelo fetch-back.
  • Datas de saque podem ter fração de segundo; as de depósito não.

Veja também