Depósitos (cash-in 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/deposits/). 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 depósito é uma intenção de cobrança: você cria o depósito, o Dracma devolve o PIX (copia-e-cola), o
jogador paga, o Dracma confirma e avisa por webhook. Você só credita o jogador quando o depósito está em
paid, lido pela API (fetch-back), nunca pelo corpo do webhook (webhooks.md).
Recorte de lançamento¶
| Método | Situação |
|---|---|
pix (BRL) |
disponível |
card, crypto, boleto, bank_transfer, wire, swift |
não disponível — responde 400 method_not_enabled |
valor fora do enum Metodo (ex.: "ted") |
400 validation_failed com errors[0] = {field: "method", reason: "enum"} |
Convenções¶
- Dinheiro é sempre
Money:{"amount": "<inteiro na menor unidade>", "currency": "BRL"}. R$ 50,00 ="5000"; R$ 0,01 ="1". Nunca float, nunca vírgula, nunca"50.00". - Tempo em RFC 3339 UTC (
2026-09-26T15:04:05Z). - IDs do Dracma são opacos com prefixo (
dep_…; hojedep_+ 32 hex). Não derive nada do formato. external_idé o seu id do depósito, único por operação (sugestão:dep_<uuidv7>).player.external_refé o seu id do jogador: opaco e imutável, nunca e-mail, telefone ou CPF.- Todo
POSTlevaIdempotency-Key(16–128 caracteres[A-Za-z0-9_.:-]). Recomendação:Idempotency-Key=external_id— assim uma repetição nunca cria cobrança duplicada. - Todo pedido é assinado (
Dracma-Key-Id,Dracma-Timestamp,Dracma-Nonce,Dracma-Signature). Nos exemplos abaixo os valores são ilustrativos; o algoritmo está em autenticacao.md.
Máquina de estados (Contrato §6.1, PIX)¶
stateDiagram-v2
[*] --> created
created --> pending: PSP devolveu o PIX
created --> failed
pending --> paid: pago, valor exato, pagador = jogador
pending --> under_review: pagador ≠ jogador / valor divergente / pago após expirar / pagador não confirmado
pending --> expired
pending --> cancelled
pending --> failed
under_review --> paid: aprovado no painel
under_review --> returned: devolvido ao pagador
paid --> refunded: estorno
paid --> chargeback: MED / chargeback
- Terminais:
expired,cancelled,failed,returned,refunded,chargeback.paidé terminal para o crédito, mas ainda pode ir arefunded/chargeback. versioncomeça em 1 (created) e sobe a cada transição; o primeiro estado que você normalmente vê épendingcomversion2. Guarde a maiorversionaplicada e ignore estado comversionmenor ou igual (webhooks não chegam em ordem — webhooks.md).refunded/chargebackestão na máquina; hoje o gatilho existe só no sandbox (POST /v1/sandbox/deposits/{id}/refund|chargeback, receita 4.12 de sandbox.md); em produção ainda não há gatilho do PSP. Implemente o tratamento assim mesmo (divergencias.md, D-D3).- Os acréscimos da v1.1.0 (
pending_compliance,frozen,paid_partial) estão em Estados futuros.
O que fazer com o saldo do jogador em cada estado¶
| Estado (lido no fetch-back) | Efeito no seu livro | Observação |
|---|---|---|
created |
nenhum | PIX ainda não gerado; espere deposit.pending ou consulte |
pending |
nenhum | mostre o copia-e-cola ao jogador até instructions.pix.expires_at |
under_review |
nenhum | o dinheiro entrou mas há divergência; motivo em review_reasons |
paid |
credita uma vez, pelo valor que você registrou na criação | confira external_id, amount, currency e player_ref com o seu registro; divergência → não credita, revisão, alerta |
expired, cancelled, failed |
nenhum | terminal sem dinheiro — mesmo que paid_amount/paid_at venham preenchidos (pagamento tardio, ver abaixo) |
returned |
nenhum | o Dracma devolveu ao pagador (terminal sem crédito) |
refunded, chargeback (depois de paid) |
débito, conforme a sua política | recomendação: não mover dinheiro automaticamente — marcar needs_decision, alertar e decidir com humano (ex.: jogador que já sacou) |
| estado desconhecido | nenhum | trate como não-terminal e confira de novo depois (Contrato §6.0) |
Regras que não mudam:
- Só
paidcredita. Empaid,paid_amounté sempre igual aamount. Se o pagador pagou valor diferente, o Dracma não vai apaid: vai aunder_reviewcomAMOUNT_MISMATCH. - Crédito é idempotente pelo
external_id: doisdeposit.paid(reentrega, job de/events, corrida) creditam uma vez só. - Pagamento depois de expirar ou cancelar. Se o PIX é pago enquanto o depósito ainda está
pendingmas depois deexpires_at, o Dracma põe emunder_reviewcomPAID_AFTER_EXPIRY. Se ele já estavaexpired/cancelled/failed, o estado não muda (não há transição no §6.1): o Dracma gravapaid_amount,paid_ate o pagador, marca para reconciliação e dispara alerta interno. Para você: não credite (o estado é terminal sem crédito); abraneeds_decision+ alerta quando virpaid_amountnum depósito terminal. - Cancelar no Dracma não cancela no PSP.
POST /deposits/{id}/cancelmuda só o estado no Dracma; o PIX continua pagável até a expiração do PSP. Um pagamento depois disso cai na regra 3 (divergencias.md, D-D2).
review_reasons e failure_code¶
Deposito.review_reasons lista por que o depósito está em under_review; fica vazio ([]) fora de
revisão.
| Código | Significado para o operador | Saída usual no painel do Dracma |
|---|---|---|
PAYER_MISMATCH |
o documento de quem pagou ≠ documento do jogador (inclui CNPJ pagando por pessoa física) | devolver ao pagador (returned) |
PAYER_UNKNOWN |
o PSP não informou o pagador. Esperado com mais frequência no Mercado Pago, que não garante o documento do pagador real (ADR-0013, MP-L2) | revisão humana |
PAYER_BLOCKED |
o documento do pagador está numa lista de bloqueio | devolver (um PIX pago não se recusa; devolve-se) |
AMOUNT_MISMATCH |
pagou valor diferente do cobrado | devolver integralmente (não existe crédito parcial em PIX) |
PAID_AFTER_EXPIRY |
pagamento confirmado depois de expires_at |
aprovar (paid) ou devolver |
RULES_UNAVAILABLE |
o motor de regras estava fora na hora do pagamento: na dúvida, revisão | revisão humana |
RULES_INVALID_RESPONSE |
o motor devolveu resposta incoerente: na dúvida, revisão | revisão humana |
CRYPTO_SOURCE_RISK |
sinal de risco de origem cripto | não se aplica a PIX; listado por completude |
RULES_UNAVAILABLE e RULES_INVALID_RESPONSE são emitidos pelo código e não estão no catálogo do REGRAS
§12 (D-D7 em divergencias.md). Pela regra do leitor tolerante, código desconhecido não muda
nada: o que manda é o status.
O campo payer (preenchido em paid/under_review) traz document, name e matches_player
(true quando o documento do pagador bate com o do jogador). Use-o só para exibir/auditar; a decisão de
crédito é o status.
failure_code em failed traz o motivo técnico. Lista aberta; exemplos que o código produz:
environment_guard (fora de produção, o PSP devolveu um PIX sem o marcador SANDBOX — o Dracma recusa
entregá-lo) e o código repassado pelo PSP (ex.: psp_rejected). Código desconhecido não muda nada: o estado
é que manda.
Criar depósito — POST /deposits¶
Escopo exigido: deposits:write.
Pedido¶
POST /v1/deposits HTTP/1.1
Host: sandbox.dracma.example
Content-Type: application/json; charset=utf-8
Idempotency-Key: dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b
Dracma-Key-Id: key_test_01J9EXEMPLO
Dracma-Timestamp: 1790000000
Dracma-Nonce: 0123456789abcdef0123456789abcdef
Dracma-Signature: v1=<assinatura>
{
"external_id": "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b",
"player": {
"external_ref": "0192a3b4-0000-7000-8000-000000000001",
"document": { "type": "CPF", "number": "12345678909", "country": "BR" },
"name": "Jogador Teste",
"email": "jogador1@operador.test",
"ip": "203.0.113.10"
},
"amount": { "amount": "5000", "currency": "BRL" },
"method": "pix",
"expires_in_seconds": 1800
}
| Campo | Obrigatório | Regra (código atual) |
|---|---|---|
external_id |
sim | ^[A-Za-z0-9_.:-]{1,64}$, único por operação |
player.external_ref |
sim | ^[A-Za-z0-9_.:-]{1,64}$, opaco e imutável |
player.document |
sim | type ∈ CPF, CNPJ, PASSPORT, NATIONAL_ID, TAX_ID; CPF/CNPJ só dígitos (11–14); country ISO alfa-2 maiúsculo. Para PIX só CPF/CNPJ são aceitos: os outros tipos passam na validação de forma e voltam 422 document_invalid |
player.email |
depende da operação | opcional no contrato; obrigatório quando a operação exige (ex.: PSP de depósito Mercado Pago) — ver a seção "Exigências por operação" abaixo. Se vier, é validado (formato, ≤ 254; fora de produção só TLD reservado) |
player.name, ip, device_id |
depende da operação | informativos/risco; a operação pode exigi-los (deposit_requirements). name ≤ 120 caracteres |
player.birth_date, kyc_level |
não | informativos |
amount |
sim | Money com amount > 0 |
method |
sim | pix |
expires_in_seconds |
não | 300–86400, default 1800 |
locale |
não | ex. pt-BR |
metadata |
não | até 20 chaves, valores string |
O documento do jogador fica fixado no primeiro uso: mandar outro documento para o mesmo
external_ref depois → 422 document_mismatch.
Validação estrita (implementada): o corpo é recusado com 400 validation_failed antes de qualquer
efeito quando há
- campo desconhecido (
errors[].reason = "unknown_field") — inclusivestatus,version,paid_amountouoperation_idaninhado. Exceção:operation_idna raiz do corpo é descartado em silêncio (o openapi diz "ignorado se existir"); tenant e operação vêm só da credencial; method_optionsefx_quote_idtambém voltamunknown_fieldhoje, embora o openapi os declare (divergencias.md, D-D4). Para PIX não há opção relevante: não os envie;- chave JSON duplicada em qualquer nível (
reason = "duplicate_key"); - UTF-8 inválido no corpo (
reason = "invalid_utf8"); - conteúdo depois do objeto JSON (
trailing_data), JSON inválido (syntax) ou tipo errado (type); - formato inválido de
external_id/external_ref/documento/país (format), tipo de documento fora da lista (enum), faixa deexpires_in_seconds/metadata(range), campo obrigatório ausente (required).
Corpo acima de 64 KiB → 413 com code = validation_failed e reason = "too_large".
Exigências por operação¶
Alguns PSPs exigem dados que o contrato deixa opcionais. As exigências dependem do PSP de depósito (cash-in) da operação, que o Dracma escolhe no onboarding:
| PSP de depósito da operação | deposit_requirements |
psp_notes |
|---|---|---|
| Mercado Pago (ADR-0013) | player.name, player.email |
payer_identity_unavailable |
| MaximaxPay (ADR-0003) | player.name |
— |
| simulador (só sandbox) | — | — |
Estado: implementado no
dracma_api(commit6d81813, tabelaoperation_method_requirement, migration 0500), conferido no código e no relatórioapi-reqem 2026-09-27. O campo é aditivo: está noopenapi.yamlque a API serve (schemaDetalheMetodo), não no contrato 1.1.0 (D-R1 em divergencias.md).
Como descobrir: GET /v1/currencies traz, em cada moeda, method_details: um item por método de
methods, na mesma ordem, com deposit_requirements (campos do Jogador que a operação exige no
POST /deposits, sempre na ordem canônica player.name, player.email, player.ip, player.device_id) e
psp_notes (avisos do PSP, códigos estáveis ^[a-z][a-z0-9_]{2,63}$, só informativos). Método sem exigência
vem com listas vazias, nunca null. Leia no boot do conector e periodicamente (ex.: a cada hora); não fixe no código.
{
"data": [
{
"code": "BRL", "kind": "fiat", "exponent": 2, "methods": ["pix"],
"method_details": [
{ "method": "pix", "deposit_requirements": ["player.name", "player.email"], "psp_notes": ["payer_identity_unavailable"] }
]
}
]
}
| Valor | Significado |
|---|---|
deposit_requirements ⊆ player.name, player.email, player.ip, player.device_id |
campo que tem de vir preenchido no POST /deposits desta operação |
psp_notes: payer_identity_unavailable (hoje a única nota) |
o PSP não informa o documento do pagador real: espere mais depósitos em under_review com PAYER_UNKNOWN |
psp_notes: código desconhecido |
informativo; ignore (leitor tolerante) |
Validação que acompanha (o POST /deposits recusa com 400 validation_failed, sem criar objeto, jogador
nem outbox, depois das checagens de método, documento, external_id e moeda, e antes do motor de regras;
a mesma Idempotency-Key devolve o mesmo corpo com Idempotent-Replayed: true):
errors[].field |
errors[].reason |
Quando |
|---|---|---|
o campo exigido (ex.: player.email) |
required_by_operation |
a operação exige e o campo veio ausente ou só com espaços |
player.email |
format |
e-mail fora de local@dominio.tld (sem nome de exibição) — vale sempre que o e-mail vier, exigido ou não |
player.email |
max_length |
mais de 254 caracteres |
player.email |
reserved_domain_required |
fora de produção (sandbox) o e-mail só pode ser de TLD reservado: .test, .invalid, .example |
player.name |
max_length |
mais de 120 caracteres (vale sempre, exigido ou não) |
{
"type": "https://docs.dracma.test/errors/validation_failed",
"title": "validation_failed",
"status": 400,
"code": "validation_failed",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"retryable": false,
"errors": [
{ "field": "player.email", "message": "obrigatório nesta operação (exigência do PSP de cash-in; ver GET /v1/currencies)", "reason": "required_by_operation" }
]
}
- É um 4xx: não cria objeto, não repete. A mesma
Idempotency-Keydevolve o mesmo400por 30 dias: corrigido o cadastro, crie o depósito comexternal_idnovo. - Mande o e-mail cadastrado do jogador, lido do seu lado servidor — nunca um e-mail digitado no corpo do pedido de depósito, e nunca um e-mail inventado.
- Sandbox/homologação: o e-mail tem de ser de domínio reservado (
jogador1@operador.test). Um cadastro de homologação copiado de produção, com e-mail real, recebereserved_domain_required: reescreva os e-mails do seu ambiente de teste. Se a operação não exige e-mail, simplesmente não envie o campo. - Antes desta exigência, a cobrança do Mercado Pago sem e-mail era criada e caía em
failed(sem chamar o PSP). Agora a recusa é síncrona e explícita. - Mande sempre
player.name: as duas operações reais (Mercado Pago e MaximaxPay) o exigem.
Resposta 201 — depósito criado com o PIX¶
{
"id": "dep_0192a3b40c6d7e8f9a0b1c2d3e4f5a6b",
"external_id": "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b",
"status": "pending",
"version": 2,
"amount": { "amount": "5000", "currency": "BRL" },
"paid_amount": null,
"method": "pix",
"player_ref": "0192a3b4-0000-7000-8000-000000000001",
"payer": null,
"instructions": {
"pix": {
"copy_paste": "00020126...SANDBOX...6304ABCD",
"expires_at": "2026-09-26T15:30:00Z"
}
},
"failure_code": null,
"review_reasons": [],
"created_at": "2026-09-26T15:00:00Z",
"updated_at": "2026-09-26T15:00:01Z",
"paid_at": null,
"expires_at": "2026-09-26T15:30:00Z"
}
instructions.pix.copy_pasteé o BR Code (payload do QR).qr_code_png_base64vem quando o PSP o fornece; você pode gerar o QR a partir docopy_paste.- Use o
instructions.pix.expires_atdevolvido, não o que você pediu: o PSP pode ajustar o prazo (o Mercado Pago só aceita de 30 min a 30 dias e prende o pedido à faixa). - Em sandbox o BR Code é sintético: recebedor fictício
sandbox@dracma.invalide o marcadorSANDBOXno campo de informação adicional. Nenhum dinheiro real se move (sandbox.md). 201comstatus: "failed"também existe: o PSP recusou a cobrança antes da resposta. O objeto foi criado e é conclusivo (vejafailure_code); não repita — crie outro depósito comexternal_idnovo se fizer sentido.
Resposta 202 — aceito, PIX ainda não disponível¶
Se o PSP demorar, o Dracma responde em ≤ 8 s (orçamento interno de 7 s) com 202, status = "created",
version = 1 e instructions = null. Não é erro: espere o webhook deposit.pending ou consulte
GET /deposits/{id} até pending, e então mostre o PIX. Não crie outro depósito para "tentar de novo".
Erros da criação (ordem em que o código confere)¶
| # | HTTP | code |
Quando | O que fazer |
|---|---|---|---|---|
| 1 | 400 | validation_failed |
corpo inválido (errors[]), inclusive required_by_operation |
corrija o pedido; não repita igual |
| 2 | 429 | rate_limited |
mais de 10 depósitos criados por jogador por hora | "tente mais tarde"; se repetir, mesma chave depois do Retry-After |
| 3 | 400 | method_not_enabled |
método do enum, mas não pix |
não ofereça o método |
| 4 | 422 | document_invalid |
tipo de documento não CPF/CNPJ, ou dígito verificador errado (ou 11111111111) |
revise o cadastro do jogador |
| 5 | 409 | duplicate_external_id |
o external_id já existe (com outra chave); o objeto vem em existing |
use o existing; não crie outro |
| 6 | 400 | currency_not_enabled / method_not_enabled |
moeda ou método não habilitados nesta operação | configuração do onboarding |
| 6b | 400 | validation_failed (required_by_operation, format, max_length, reserved_domain_required) |
exigência da operação (seção acima) | corrija o cadastro; external_id novo |
| 7 | 422 | amount_below_minimum / amount_above_maximum / document_mismatch / player_blocked / destination_blocked / velocity_exceeded |
recusa do motor de regras (sem objeto) | mostre a mensagem genérica ao jogador; não revele lista |
| 8 | 503 | service_unavailable |
motor de regras, cofre ou banco fora | repita com a mesma Idempotency-Key após Retry-After |
velocity_exceeded no depósito cobre DEPOSIT_WINDOW_EXCEEDED e DEPOSIT_VELOCITY_UNPAID (muitos
depósitos criados e não pagos numa janela; o limiar é da regra da operação).
Recusa 4xx não cria objeto. A mesma Idempotency-Key devolve a mesma recusa (guardada 30 dias). Se
você usa Idempotency-Key = external_id, tentar de novo depois de uma recusa exige external_id novo.
Detalhes e regra de ouro em erros-e-idempotencia.md.
Consultar depósito — GET /deposits/{id}¶
Escopo: read. É o fetch-back: o estado autoritativo usado antes de creditar.
GET /v1/deposits/dep_0192a3b40c6d7e8f9a0b1c2d3e4f5a6b HTTP/1.1
Host: sandbox.dracma.example
Dracma-Key-Id: key_test_01J9EXEMPLO
Dracma-Timestamp: 1790000100
Dracma-Nonce: <32 hex CSPRNG>
Dracma-Signature: v1=<assinatura>
- 200 → objeto
Deposito(mesmo formato do 201). - 404
not_found→ não existe nesta operação (objeto de outra operação também é 404, nunca 403); id fora do formatodep_[A-Za-z0-9]+também é 404.
No fetch-back, antes de creditar, confira contra o que você gravou na criação:
external_id é seu, amount.amount e amount.currency idênticos, player_ref idêntico. Qualquer
divergência: não credite, marque para revisão e alerte.
Listar depósitos — GET /deposits¶
Escopo: read. Paginação por cursor, em ordem estável. Balde de listagem: 5 req/s por chave.
| Parâmetro | Uso |
|---|---|
external_id |
acha o depósito pelo seu id (útil depois de timeout no POST) |
player_ref |
depósitos de um jogador |
status |
filtra por estado (estado fora do enum → 400, reason: format) |
updated_since |
RFC 3339; varredura incremental |
cursor |
valor de next_cursor da página anterior (hoje é o id dep_… do último item; trate como opaco) |
limit |
1–200, default 50 |
GET /v1/deposits?external_id=dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b&limit=10 HTTP/1.1
Resposta: {"data": [Deposito…], "next_cursor": "…" | null}. Assine o path com a query exatamente como
enviada (autenticacao.md).
Cancelar depósito — POST /deposits/{id}/cancel¶
Escopo: deposits:write. Corpo vazio. Idempotency-Key obrigatória.
POST /v1/deposits/dep_0192a3b40c6d7e8f9a0b1c2d3e4f5a6b/cancel HTTP/1.1
Idempotency-Key: cnl_dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b
Dracma-Key-Id: key_test_01J9EXEMPLO
Dracma-Timestamp: 1790000200
Dracma-Nonce: <32 hex CSPRNG>
Dracma-Signature: v1=<assinatura>
- Cancela só de
pending. 200 com o objeto emcancelled— ou o mesmo 200 (sem evento novo) se ele já estava cancelado. created,paid,expired,under_reviewou qualquer outro →409 invalid_state_transition. Emcreated, esperependinge cancele depois.- Cancelar não impede que um PIX já em voo seja liquidado (regra 4 acima): siga "só
paidcredita".
Expiração¶
O Dracma expira sozinho os depósitos pending depois de expires_at + 2 min de folga (o job roda a cada
30 s). A folga existe porque o PSP ainda pode aceitar o pagamento logo depois do prazo. Ao expirar sai o
webhook deposit.expired.
Fluxo recomendado¶
- Grave localmente o depósito (
external_id,amount,player_ref) antes de chamar o Dracma. POST /depositscomIdempotency-Key = external_id. Timeout de cliente 10 s.- Erro de rede, 5xx ou 429 → repita com a mesma chave até resposta conclusiva (ou consulte
GET /deposits?external_id=). - Mostre
instructions.pix.copy_pasteatéinstructions.pix.expires_at. - Webhook
deposit.paid→ verifique a assinatura, deduplique, faça fetch-back e só então credite (webhooks.md). - Job a cada 5 min com
GET /events?after=e a conciliação diária fecham lacunas (reconciliacao.md).
Como testar cada caminho (pagar, pagar com outro CPF, pagar valor diferente, expirar, outage) em sandbox: sandbox.md.
Estados futuros (leitor tolerante)¶
A v1.1.0 do contrato acrescentou estados de depósito para métodos que não estão disponíveis ainda
(wire/swift) ou atrás de regra desligada: pending_compliance (fundos em screening), frozen
(retido por sanção/ordem legal) e paid_partial (aceito valor menor). Eventos correspondentes:
deposit.pending_compliance, deposit.frozen, deposit.paid_partial.
Regra (Contrato §6.0) que o seu código DEVE seguir desde já:
- estado ou
typede evento desconhecido = não-terminal, não move dinheiro (nem crédito, nem débito); o webhook é respondido 200 e deduplicado normalmente; confira de novo depois pelo fetch-back e porGET /events?after=; - campo JSON desconhecido é ignorado — nunca falhe o parse; valor desconhecido de
method,review_reasonsoufailure_codetambém.
pending_compliance pode aparecer em qualquer método quando uma lista de sanções casa; com a regra acima,
nada se move no seu lado.