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_…; hoje dep_ + 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 POST leva Idempotency-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 a refunded/chargeback.
  • version começa em 1 (created) e sobe a cada transição; o primeiro estado que você normalmente vê é pending com version 2. Guarde a maior version aplicada e ignore estado com version menor ou igual (webhooks não chegam em ordem — webhooks.md).
  • refunded/chargeback estã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:

  1. Só paid credita. Em paid, paid_amount é sempre igual a amount. Se o pagador pagou valor diferente, o Dracma não vai a paid: vai a under_review com AMOUNT_MISMATCH.
  2. Crédito é idempotente pelo external_id: dois deposit.paid (reentrega, job de /events, corrida) creditam uma vez só.
  3. Pagamento depois de expirar ou cancelar. Se o PIX é pago enquanto o depósito ainda está pending mas depois de expires_at, o Dracma põe em under_review com PAID_AFTER_EXPIRY. Se ele já estava expired/cancelled/failed, o estado não muda (não há transição no §6.1): o Dracma grava paid_amount, paid_at e o pagador, marca para reconciliação e dispara alerta interno. Para você: não credite (o estado é terminal sem crédito); abra needs_decision + alerta quando vir paid_amount num depósito terminal.
  4. Cancelar no Dracma não cancela no PSP. POST /deposits/{id}/cancel muda 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") — inclusive status, version, paid_amount ou operation_id aninhado. Exceção: operation_id na raiz do corpo é descartado em silêncio (o openapi diz "ignorado se existir"); tenant e operação vêm só da credencial;
  • method_options e fx_quote_id também voltam unknown_field hoje, 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 de expires_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 (commit 6d81813, tabela operation_method_requirement, migration 0500), conferido no código e no relatório api-req em 2026-09-27. O campo é aditivo: está no openapi.yaml que a API serve (schema DetalheMetodo), 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-Key devolve o mesmo 400 por 30 dias: corrigido o cadastro, crie o depósito com external_id novo.
  • 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, recebe reserved_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_base64 vem quando o PSP o fornece; você pode gerar o QR a partir do copy_paste.
  • Use o instructions.pix.expires_at devolvido, 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.invalid e o marcador SANDBOX no campo de informação adicional. Nenhum dinheiro real se move (sandbox.md).
  • 201 com status: "failed" também existe: o PSP recusou a cobrança antes da resposta. O objeto foi criado e é conclusivo (veja failure_code); não repita — crie outro depósito com external_id novo 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 formato dep_[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 em cancelled — ou o mesmo 200 (sem evento novo) se ele já estava cancelado.
  • created, paid, expired, under_review ou qualquer outro → 409 invalid_state_transition. Em created, espere pending e cancele depois.
  • Cancelar não impede que um PIX já em voo seja liquidado (regra 4 acima): siga "só paid credita".

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

  1. Grave localmente o depósito (external_id, amount, player_ref) antes de chamar o Dracma.
  2. POST /deposits com Idempotency-Key = external_id. Timeout de cliente 10 s.
  3. Erro de rede, 5xx ou 429 → repita com a mesma chave até resposta conclusiva (ou consulte GET /deposits?external_id=).
  4. Mostre instructions.pix.copy_paste até instructions.pix.expires_at.
  5. Webhook deposit.paid → verifique a assinatura, deduplique, faça fetch-back e só então credite (webhooks.md).
  6. 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 type de 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 por GET /events?after=;
  • campo JSON desconhecido é ignorado — nunca falhe o parse; valor desconhecido de method, review_reasons ou failure_code també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.